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": "..."} 示例: 问题:查询上个月每天的订单数 SQL:SELECT 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.89,0.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, timeout=30) except Exception as e: # 把错误信息回传给模型,让它修正 fixed = await fix_sql(sql, str(e), relevant_tables) rows = execute_sql(fixed["sql"], session.datasource, timeout=30) 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——只要做稳了,剩下的都是锦上添花。