
1. 为什么我要做 Stela 这个 AI 数据工作台先说说我做 Stela 的起因。团队里每天都有运营、产品、市场的人跑来问数据“上周新增用户多少”“哪个渠道的转化掉了”“帮我把这张表导出来”。这些问题本身不复杂但每问一次数据同学就得写一遍 SQL、跑一遍、截图、贴到聊天窗口。一天下来真正做分析的时间被切得稀碎。更麻烦的是很多需求方其实自己也能看懂数据只是卡在“不会写 SQL”和“不知道表结构”这两道门槛上。Stela 就是冲着这个场景去的。它本质上是一个AI 数据工作台你用自然语言描述想要什么数据它帮你生成 SQL、执行查询、把结果整理成 Markdown 表格还能继续追问、做二次分析。核心关键词就几个——Stela、AI、SQL、Markdown、Data Agent。它解决的不是“让 AI 取代数据分析师”而是“把重复的取数工作自动化让人专注在判断和决策上”。适合谁来参考这篇内容三类人。第一类是数据团队的同学想给自己团队搭一个内部取数工具第二类是做 AI Agent 方向的开发者想看看一个真实可用的 Data Agent 是怎么落地的第三类是对 AI 编程感兴趣、想动手做点东西的独立开发者。不管你 SQL 水平如何只要你能看懂基本的表结构这篇内容都能给你一套可复现的思路。我先把结论摆在这Stela 的技术栈并不复杂难的是把“自然语言到 SQL 再到结果呈现”这条链路做稳。下面我会从整体设计、核心细节、实操过程、问题排查四个维度把踩过的坑和验证过的方案完整讲一遍。2. Stela 的整体设计与思路拆解2.1 为什么是“工作台”而不是“聊天机器人”市面上很多 AI 取数工具做成了纯聊天窗口用户问一句、AI 答一句。我一开始也想过这么做但实测下来问题很明显数据查询是一个多步骤、需要上下文的过程。用户第一句问“上个月销售额”第二句可能问“那环比呢”第三句可能说“把下降最多的品类列出来”。如果每一轮都当成独立对话AI 就得反复猜表结构、反复确认口径体验很差。所以我把它定位成“工作台”而不是“聊天机器人”。工作台意味着左侧是数据源和表结构中间是对话和 SQL 编辑区右侧是结果展示区。用户能看到 AI 生成的 SQL能手动改能保存查询能基于结果继续追问。这个设计背后的逻辑是——AI 负责降低门槛但人始终保留控制权。数据这东西一旦 AI 悄悄改了口径、算错了聚合后果比不会写 SQL 严重得多。提示做 Data Agent 类产品一定要让 SQL 可见、可编辑、可追溯。把 AI 当成副驾驶而不是自动驾驶。2.2 技术选型为什么选这些组件Stela 的核心链路是自然语言 → SQL → 执行 → Markdown 呈现。围绕这条链路我做了几轮选型。大模型层我试过几种方案最后选择支持函数调用Function Calling的模型。原因是 Data Agent 需要模型输出结构化的东西比如{sql: ..., explanation: ...}而不是一段自由文本。用函数调用能让输出格式稳定很多解析起来不容易出错。如果模型不支持函数调用退而求其次用 JSON 模式加严格的提示词约束但稳定性会差一截。数据库层初期我用的是 SQLite方便本地跑通。后来接入真实业务换成了 PostgreSQL 和 SQL Server 两种。这里有个经验——不同数据库的 SQL 方言差异很大比如分页、日期函数、字符串拼接都不一样。Stela 的做法是在提示词里注入当前数据源的方言信息让模型生成对应方言的 SQL。这一步不做生成的 SQL 十有八九跑不通。结果呈现层为什么用 Markdown 而不是直接渲染成图表因为 Markdown 表格的通用性最强。用户可以把结果直接复制到文档、聊天窗口、邮件里格式不会乱。而且 Markdown 表格转 Excel 也很方便很多在线工具一键就能转。对于需要图表的情况我在结果区额外提供了简单的柱状图和折线图但默认还是 Markdown 表格。前端层我用的是 React 加一个 Markdown 渲染器。这里踩过一个坑——Markdown 表格的换行和转义很容易出问题。比如单元格里如果有竖线|表格就会错位。解决办法是在生成 Markdown 之前对单元格内容做转义处理把|替换成\|。2.3 整体架构一条清晰的数据流Stela 的架构可以拆成四层。第一层是接入层负责接收用户输入、管理会话上下文。这里的关键是维护一个“对话历史 当前数据源 schema”的上下文包每次请求都带上。第二层是理解层也就是大模型。它接收上下文输出结构化的 SQL 和解释。这一层要做的事包括识别用户意图、匹配相关表、生成 SQL、解释 SQL 在做什么。第三层是执行层负责连接数据库、执行 SQL、处理错误。这里必须做 SQL 安全校验比如只允许 SELECT 语句禁止 DROP、DELETE、UPDATE 等写操作。我见过有人图省事直接执行模型生成的任何 SQL结果模型抽风生成了一条 DELETE数据就没了。第四层是呈现层把查询结果转成 Markdown 表格附带行数、耗时、SQL 原文。如果结果为空要给出友好提示而不是甩一个空表格。这四层之间通过明确的接口通信每一层都可以单独替换。比如你想换个模型只动理解层想换个数据库只动执行层。这种解耦在后期迭代时省了很多事。3. 核心细节解析与实操要点3.1 提示词工程让模型稳定输出 SQL提示词是 Stela 的灵魂。我前后改了十几版总结出几个关键点。第一schema 要精简但完整。不要把整个数据库的所有表都塞进提示词那样 token 消耗大模型还容易分心。我的做法是先用一个轻量模型或关键词匹配从用户问题里提取可能的表名再把相关表的 schema 注入。比如用户问“订单”就只注入订单表、用户表、商品表的结构。第二给示例但别给太多。Few-shot 示例能显著提升 SQL 准确率但示例太多会占满上下文。我的经验是给 3 到 5 个覆盖典型场景的示例简单查询、聚合、多表 join、日期过滤、排序取 Top N。每个示例包含“问题 SQL”两行就够。第三明确约束。提示词里要写清楚只生成 SELECT 语句、字段名必须来自 schema、日期格式用哪种、空值怎么处理。这些约束不写模型就会自由发挥。下面是我实际用的提示词骨架你可以直接参考你是一个 SQL 生成助手。根据用户问题和给定的表结构生成一条可执行的 SELECT 查询。 表结构 {schema} 规则 1. 只生成 SELECT 语句禁止任何写操作。 2. 字段名必须严格来自上面的表结构。 3. 日期过滤使用 {date_format} 格式。 4. 如果用户问题有歧义在 explanation 里说明你的假设。 5. 输出格式为 JSON{sql: ..., explanation: ...} 示例 问题查询上个月每天的订单数 SQLSELECT DATE(created_at) AS day, COUNT(*) AS order_count FROM orders WHERE created_at 2026-01-01 AND created_at 2026-02-01 GROUP BY DATE(created_at) ORDER BY day;注意提示词里的日期范围不要写死要用变量注入。我一开始写死了示例日期结果模型经常照抄示例里的日期闹了不少笑话。3.2 SQL 安全校验别让 AI 有写权限这一块我要单独拎出来讲因为它太重要了。模型生成的 SQL 必须经过校验才能执行。我的校验分三步。第一步是语法解析。用一个 SQL parser 把生成的语句解析成 AST检查根节点是不是 SELECT。如果是 INSERT、UPDATE、DELETE、DROP、ALTER 等直接拒绝。这一步能挡住绝大多数危险操作。第二步是关键词黑名单。即使解析出来是 SELECT也要检查有没有嵌套的危险操作比如SELECT ... INTO OUTFILE、SELECT ... FOR UPDATE。这些在 MySQL 里能造成副作用。第三步是执行超时和行数限制。给查询设置一个超时时间比如 30 秒超过就中断。同时限制返回行数比如最多 10000 行避免一个大查询把内存打爆。FORBIDDEN_KEYWORDS [insert, update, delete, drop, alter, truncate, into outfile, for update] def validate_sql(sql: str) - bool: lowered sql.lower() for kw in FORBIDDEN_KEYWORDS: if kw in lowered: return False # 进一步用 parser 校验根节点 return is_select_statement(sql)这套校验下来基本能保证 AI 只能在只读范围内活动。数据库账号本身也要用只读账号双保险。3.3 Markdown 结果呈现细节决定体验查询结果转 Markdown 表格看起来简单其实细节很多。列名处理数据库返回的列名可能是count(*)这种带特殊字符的直接放进 Markdown 表格会很难看。我的做法是让模型在生成 SQL 时就给每个字段起别名比如COUNT(*) AS order_count。如果模型没起别名后端再做一次清洗把特殊字符替换掉。空值处理数据库里的 NULL 在 Markdown 里显示成什么我试过显示NULL、显示空字符串、显示-最后选了-因为它在表格里最不突兀用户一眼就知道是空值。长文本截断如果某个字段是长文本比如备注、描述直接放进表格会把表格撑得特别宽。我的做法是超过 50 个字符就截断后面加省略号鼠标悬停显示完整内容。数字格式化金额、百分比这些要格式化。比如1234567.89显示成1,234,567.890.1234显示成12.34%。这个格式化规则可以根据字段名自动推断比如字段名含rate、ratio、percent就按百分比处理。Markdown 表格转 Excel很多用户查完数据想导出 Excel。Markdown 表格转 Excel 有个简单办法——把 Markdown 表格复制到支持 Markdown 的编辑器里再复制到 Excel或者用在线转换工具。Stela 里我直接提供了一个“复制为 TSV”的按钮TSV 粘贴到 Excel 里会自动分列比 Markdown 转换还方便。3.4 多轮对话与上下文管理Data Agent 和普通聊天机器人的区别在于它需要记住“当前在查什么数据”。用户问“上个月销售额”接着问“那前个月呢”AI 得知道“那”指的是销售额。我的做法是维护一个结构化的会话状态而不是简单地把历史消息拼起来。状态里包含当前数据源、当前涉及的表、上一次的 SQL、上一次的结果摘要。每次新问题进来把这些状态和问题一起送给模型。这样做的好处是上下文更干净。如果只拼历史消息几轮之后 token 就爆了而且模型容易被早期无关信息干扰。结构化状态能让模型聚焦在当前查询上。实操心得会话状态里一定要存“上一次的 SQL”。用户经常说“在这个基础上加个条件”有了上一次的 SQL模型改起来准确率高很多。4. 实操过程与核心环节实现4.1 环境准备与依赖安装先把环境搭起来。我用的是 Python 后端加 React 前端数据库先用 SQLite 跑通再换 PostgreSQL。后端依赖pip install fastapi uvicorn sqlalchemy openai sqlparsefastapi和uvicorn提供 Web 服务。sqlalchemy做数据库连接和查询。openai是模型调用 SDK换成其他模型也类似。sqlparse用来做 SQL 解析和校验。前端依赖npm install react react-dom react-markdown remark-gfmremark-gfm是必须的它让 Markdown 渲染器支持表格。不加这个表格会渲染成一堆竖线。数据库我用 SQLite 起步建两张测试表CREATE TABLE orders ( id INTEGER PRIMARY KEY, user_id INTEGER, amount DECIMAL(10,2), category TEXT, created_at DATETIME ); CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT, channel TEXT, registered_at DATETIME );塞点测试数据几百行就够验证链路了。4.2 核心链路从问题到结果整个链路的核心函数大概长这样async def handle_query(question: str, session: Session): # 1. 提取相关表 relevant_tables match_tables(question, session.datasource) # 2. 构建提示词 prompt build_prompt(question, relevant_tables, session.history) # 3. 调用模型 response await call_llm(prompt) sql response[sql] # 4. 安全校验 if not validate_sql(sql): return {error: 生成的 SQL 未通过安全校验} # 5. 执行查询 try: rows execute_sql(sql, session.datasource, timeout30) except Exception as e: # 把错误信息回传给模型让它修正 fixed await fix_sql(sql, str(e), relevant_tables) rows execute_sql(fixed[sql], session.datasource, timeout30) sql fixed[sql] # 6. 转 Markdown markdown to_markdown(rows) # 7. 更新会话状态 session.history.append({question: question, sql: sql}) return {sql: sql, markdown: markdown, row_count: len(rows)}这里面有两个关键设计。一是表匹配不要把所有表都塞给模型。我用的是简单的关键词加字段名匹配比如问题里出现“订单”“金额”就匹配到 orders 表。复杂场景可以用向量检索但对大多数内部工具来说关键词匹配够用了。二是错误自修复。SQL 执行失败很常见比如字段名拼错、函数用错。与其直接报错给用户不如把错误信息回传给模型让它改一版再试。实测下来大部分语法错误一次就能修好。4.3 参数计算与选择过程这里说几个需要算参数的地方。超时时间我设的是 30 秒。为什么不是 10 秒或 60 秒因为内部数据查询大多是聚合10 秒对复杂查询不够60 秒用户等得心焦。30 秒是个平衡点。如果经常超时说明该加索引了而不是调大超时。返回行数上限10000 行。这个数字是拍脑袋定的吗不是。我算过一行数据平均 200 字节10000 行大概 2MB转成 Markdown 后大概 3 到 4MB前端渲染压力可控。超过这个量用户其实也不会在表格里看应该导出或者做聚合。上下文 token 预算模型上下文有限我给它分配的比例是——schema 占 30%示例占 20%对话历史占 30%当前问题占 20%。如果超了优先裁剪对话历史保留最近的几轮。表匹配数量最多匹配 5 张表。超过 5 张join 关系会变得复杂模型容易出错。如果确实需要更多表说明这个查询本身就该拆成多步。4.4 前端交互的关键实现前端有两个地方值得说。SQL 编辑区AI 生成的 SQL 要展示在一个可编辑的代码框里。用户改完点“重新执行”就能跑改后的 SQL。这个功能看似简单但极大提升了信任感——用户能看到 AI 到底写了什么也能自己修正。结果区渲染用react-markdown加remark-gfm渲染表格。要注意的是表格外面要包一个可横向滚动的容器否则列多了会撑破布局。div style{{ overflowX: auto }} ReactMarkdown remarkPlugins{[remarkGfm]} {markdownContent} /ReactMarkdown /div加载状态查询可能要几秒必须有 loading 提示。我一开始没做用户以为卡死了反复点查询按钮结果发了一堆重复请求。后来加了 loading 状态和按钮禁用问题就没了。5. 常见问题与排查技巧实录5.1 SQL 生成错误的典型场景这是最高频的问题。我整理了一张速查表问题现象可能原因解决办法字段名不存在schema 没注入全或模型幻觉检查 schema 注入提示词强调字段必须来自 schema日期格式错误方言不匹配提示词里注入当前数据库的日期格式聚合函数用错问题理解偏差在 explanation 里让模型说明假设用户可纠正join 条件缺失多表关系不明确schema 里补充外键信息分页语法错误方言差异按数据库类型注入分页语法示例中文别名乱码编码问题统一用英文别名前端做映射我遇到最多的是字段名幻觉。模型会编一个看起来很像但不存在的字段名比如把created_at写成create_time。解决办法是在提示词里把可用字段列成清单并强调“只能使用清单中的字段”。5.2 数据库连接与驱动问题接入 SQL Server 时踩过一个坑报错信息是“驱动程序无法通过使用安全套接字层加密与 SQL Server 建立安全连接”。这个问题的根源是驱动版本和加密配置不匹配。解决办法是更新驱动并在连接字符串里明确加密参数。这类问题在接入企业内网数据库时很常见建议提前和 DBA 确认连接参数。另一个常见问题是连接池耗尽。Stela 是 Web 服务多个用户同时查询会占用多个连接。如果连接池太小后来的请求就会排队甚至超时。我的做法是把连接池设成 10 到 20并设置连接回收时间避免空闲连接一直占着。5.3 慢查询与性能优化AI 生成的 SQL 不一定是最优的。我见过模型生成一个没有索引字段过滤的全表扫描几百万行数据跑了十几秒。优化思路有几个。第一在 schema 里标注索引字段。告诉模型哪些字段有索引让它优先用这些字段做过滤。第二加查询超时和行数限制前面说过了。第三对高频查询做缓存。同样的 SQL 在短时间内重复执行直接返回缓存结果。缓存 key 用 SQL 原文加数据源。第四定期分析慢查询日志把高频的慢 SQL 拿出来要么加索引要么在提示词里给模型更明确的指引。实操心得慢查询优化不要只盯着数据库。有时候是模型生成的 SQL 本身就不合理比如该用WHERE过滤的用了HAVING该用JOIN的用了子查询。在提示词里加几条“性能建议”能减少不少慢查询。5.4 Markdown 渲染的坑Markdown 表格渲染有几个经典坑。换行问题Markdown 里单元格内换行要用br直接回车会破坏表格结构。如果数据里有换行符要替换成br。竖线转义单元格内容含|要转义成\|否则表格列数会错乱。表格转换 Excel 丢格式Markdown 表格转 Excel 时数字经常被当成文本。解决办法是导出时用 TSV 格式Excel 能自动识别数字类型。数学符号如果数据里有$、\这些符号Markdown 渲染器可能当成数学公式或转义符。要么转义要么在渲染器里关掉数学支持。5.5 会话状态丢失与并发问题多用户场景下会话状态管理容易出问题。我一开始把状态存在内存里结果服务重启就全丢了。后来改成存 Redis并给每个会话设置过期时间比如 2 小时。并发问题也要注意。同一个用户快速连发两条消息如果两条消息同时处理会话状态可能互相覆盖。解决办法是给每个会话加锁同一会话的请求串行处理。6. 我对 Stela 这类工具的一些真实体会做 Stela 这段时间最大的感受是AI 取数工具的难点不在 AI而在数据治理。模型再强如果表结构混乱、字段命名随意、口径不统一生成的 SQL 照样是错的。我见过一个团队同一个“活跃用户”在三个表里有三种定义AI 根本不知道该用哪个。所以做这类工具之前先把数据字典理清楚比调模型重要得多。另一个体会是不要追求 100% 的准确率。AI 生成 SQL 有 80% 到 90% 的准确率就已经很实用了剩下的靠用户手动修正。把 SQL 展示出来、允许编辑比追求全自动更靠谱。用户要的是“省事”不是“完全不用管”。最后分享一个小技巧在结果区加一个“这条 SQL 对吗”的反馈按钮。用户点“不对”就把这条问题和 SQL 记下来定期 review用来优化提示词。这个反馈闭环跑起来之后准确率会肉眼可见地提升。我自己的经验是跑了两周反馈常见问题的 SQL 准确率从 70% 多提到了 90% 以上。这个工具后续还能往几个方向扩展。比如接入更多数据源、支持定时查询和告警、把常用查询保存成模板。但核心链路——自然语言到 SQL 到 Markdown——只要做稳了剩下的都是锦上添花。