别人都在教你怎么写Agent,而我整理了我踩过的坑,本篇为失败复盘
先说结论:为了保证数据分析的结果100%可复现,可验证,同一份数据无论你怎么去分析,她都100%复现结果“我提前内置了100+数据分析的算法,让agent直接调用,纯白箱操作”
正文·序
最近一直在写数据分析 Agent。
我一开始真以为这玩意不难。
不就是给 SPSSGO 接个大模型吗?让它看数据、看问卷、跟用户聊几句,然后自动生成分析方案,点一下开始跑,最后写报告。
结因为统计分析这个场景太不一样了。它不是“回答得差不多就行”,而是同一份数据、同一个方案,最后结果必须稳定。今天 AI 想做 Pearson,明天它突然想换 Spearman,这在聊天产品里叫灵活,在数据分析产品里叫事故。
结果真写起来才发现,这东西最难的根本不是“让 AI 动起来”,而是“别让 AI 乱动”。
统计分析不是普通聊天。用户上传同一份数据,提出同一个研究问题,系统最后跑出来的结果必须可复现。今天跑一次、明天跑一次、换个人跑一次,不能因为模型当时心情不同,方法就变了,变量就换了,报告结论就飘了。
所以这个 Agent 最后不是一个“聪明助手”,更像一个被关进轨道里的调度员。
AI 可以理解需求,可以帮用户把研究目标、变量关系、假设、题组归属说清楚;但真正执行时,它只能从系统已经注册好的分析方法里选,不能自己写代码,不能自己造方法,不能因为觉得用户选的方法“不太合适”就偷偷换成别的。
这条路是被坑一点点逼出来的。
第一阶段:我以为接 Agent 框架就够了
早期方案里,我甚至认真规划过 AgentScope 2.0。
想法是:用 ReActAgent,配几个工具:
- 看数据结构
- 看问卷内容
- 列变量
- 生成分析计划
- 执行分析计划
- 保存问卷文本
当时的思路很诱人:不写复杂状态机,不自己维护推理循环,不自己搞事件流,AgentScope 负责 ReAct、工具调用、流式回复、用户确认。
说白了,就是想偷个懒:把“智能体”这件事交给框架。
后来发现,这个方向只适合做“对话入口”,不适合直接托管统计分析的核心流程。
因为一旦你把“生成方案”和“执行分析”都暴露给 Agent,它就会开始出现几类很要命的问题:
- 它会说“我已经生成方案了”,但后端其实没有落库。
- 它会说“正在执行分析”,但 job 队列里根本没有任务。
- 它会把用户明确说的 T 检验换成它觉得更稳妥的非参数检验。
- 它会输出一个看起来很专业、但系统里根本不存在的分析方法。
- 它会把一段聊天当成事实,把一个建议当成已经执行的动作。
普通 AI 助手这么说,顶多是“有点幻觉”。统计分析产品这么干,就是事故。
所以后面第一刀就砍得很狠:
聊天 Agent 只允许读数据、读问卷、读参考资料、记录研究目标、记录变量映射、记录假设。
“生成方案”和“开始分析”必须由用户在页面上点按钮触发,走固定 HTTP 接口和后端 job 管线。
这一步其实挺反直觉。你做的是 Agent,结果你先把 Agent 的手砍掉一半。但不这么砍,后面根本不可控。
第二阶段:用户说做什么,就必须做什么
这里有个坑非常典型。
用户说:“做独立样本 T 检验。”
模型看完数据后可能会说:“这个数据可能不满足正态性,我建议改用 Mann-Whitney U 检验。”
从统计建议角度,它甚至不一定错。
但从产品角度,这就是越权。
用户大多数情况下知道自己要做什么。SPSSGO 的 Agent 不是导师,也不是审稿人,它是工具。用户明确指定方法时,AI 必须照办。担忧可以写在风险提示里,不能擅自换方法。
所以现在这条规则被写成了硬边界:
- 用户点名的方法必须进入方案。
- 不允许因为统计假设、样本条件、模型偏好而替换。
- 如果用户只是问风险,比如“如果不满足正态性怎么办”,AI 可以补充风险说明,但不能直接把方法 key 改掉。
- 只有用户明确说“改用 Spearman”“换成 Mann-Whitney U”,才允许替换。
这不是 prompt 里写一句“请不要乱改”就完事。
后端还做了硬拦截:AI 生成局部修改 proposal 时,如果它想改 method_key,系统会判断用户指令里有没有明确点名新方法。没点名就拒绝。
这类规则如果只靠模型自觉,迟早会翻车。
第三阶段:黑箱代码路径被废掉
SPSSGO 早期有一条很常见的 AI 分析路线:模板引擎优先,如果模板覆盖不了,就让 AI 生成 Python 代码,最多重试几次。
这条路线做 demo 很爽。
但做 Agent 工作流就很危险。
因为你没法保证:
- 这次生成的代码和下次一样。
- 代码有没有偷偷改数据。
- 代码有没有绕过安全边界。
- 报告里的结果能不能追溯到一个稳定方法。
- 用户回头复现时还能不能得到同样结果。
所以智能分析 Agent 最后走的是白箱路线:
所有可执行能力必须来自 METHOD_REGISTRY。
LLM 只负责把用户需求翻译成结构化方案,比如:
- 用哪个
method_key - 这一步的目的是什么
- 用哪些变量
- 每个 slot 填什么
- option 参数是什么
- 和前后步骤有什么依赖
真正执行时,统一走 submit_execute_method_job,和普通数据分析 tab 使用同一条执行管线。
这件事很关键。
Agent 不是单独开一条“AI 专属分析路径”。它只是给现有白箱分析能力加了一个自然语言入口。
第四阶段:一次性大 Prompt 把自己写炸了
方案生成一开始是最容易出问题的地方。
最早的思路很粗暴:把用户研究目标、变量说明、问卷文本、参考资料、数据摘要、完整方法字典,全塞给 LLM,让它一次性吐出完整 JSON 方案。
然后就开始经典痛苦:
- 输出太长,JSON 尾巴断了。
recommended_methods还没写完就被截断。variable_mapping展开一堆变量,把真正执行需要的步骤挤没了。- LLM 把中文标点吐进 JSON。
- LLM 多吐尾逗号。
- LLM 用 Markdown 代码块包 JSON。
- LLM 输出空壳:```json 然后里面没东西。
- 报错只显示“原文片段:”,后面是空的,根本不知道哪一步挂了。
这个地方真的很烦。
因为表面上看是“LLM 输出不是 JSON”,但实际原因可能完全不同:
- 模型被选成 thinking / reasoning 型,答案跑到
reasoning_content里。 max_tokens被思考过程吃光,正文一个字没写。- 上游限流或网络抖动。
- 输出太长被截断。
- prompt 前缀里混了动态字段,缓存命中率掉下去,成本也上来。
后来方案生成被拆成 funnel:
- 先选分析大类。
- 再在大类里选候选方法。
- 再生成方案骨架。
- 最后每个方法单独填 slot 和 option。
也就是把“让 LLM 一口气写完整方案”,改成“让 LLM 每次只干一小件事”。
这一步改完以后,稳定性明显上来了。
因为单方法填充最多就几百 token,失败也只影响一个方法。某个方法填不出来,就降级成一个空 slot 的占位步骤,让校验器把问题推给用户补,而不是整套方案直接死。
第五阶段:trace 是被失败逼出来的
这里有一条本地设计记录写得很真实:
用户反馈“步骤 4(方案规划)”反复失败,错误信息只显示
原文片段:后面为空,无法判断是哪个 LLM 调用、哪个阶段、因为什么原因挂掉。
这个问题很要命。
一个方案规划链路里可能有好几次 LLM 调用:
- 大类选择
- 方法选择
- 方法选择 fallback
- 骨架生成
- 骨架重试
- 单方法填充
- 单方法重试
- legacy strict / compact / extreme 三档兜底
如果只在控制台打一行 warning,后面根本查不出来是哪一步先坏的。
所以后来加了 agent_plan_generation_traces。
每次生成方案都会生成一个 trace_id。同一次规划里,每一次 LLM 调用都落一条 trace:
- stage
- attempt_index
- method_key
- prompt_full
- response_full
- model
- finish_reason
- token usage
- parse_status
- error
失败时错误文案带上 [trace_id=...]。用户把 trace_id 发回来,管理员就能查完整链路。
这个功能看起来不酷,但它是 Agent 能从“玄学调 prompt”走向工程排障的分水岭。
没有 trace,所有问题都像“模型又抽风了”。
有了 trace,才能知道到底是 selector 没选出来,还是 skeleton 空响应,还是某个 method_fill 解析失败,还是 legacy 输出被截断。
第六阶段:列名和题面,能把用户绕晕
问卷数据还有一个特别具体的坑:列名。
系统内部喜欢用短列名,比如 col_3、q2_1、x1。这对程序很方便,对用户很崩溃。
用户看的是题面:“我对当前工作要求感到满意”。你跟他说 col_11 全部是 7 分,他根本不知道你在说哪一题。
所以后面加了列别名机制:
name是系统内部短标识,给 slot 和后端校验用。label是题面或原始列名,给用户看。- LLM 在 slot 里只能填
name,不能填题面文本。 - 出口展示时再把 alias 翻译回 label。
甚至 prompt 里专门写了:不准对用户念 col_3 这种内部编号。
这不是洁癖。
如果这里不管,前端会出现两种灾难:
- LLM 把题面长文本填进 slot,后端校验列名不存在。
- LLM 对用户解释时露出内部短码,用户完全不知道自己确认了什么。
第七阶段:量表题组不能靠猜
问卷类数据里,信度、效度、EFA、CFA、SEM 都绕不开题组归属。
哪些题属于同一个维度?
哪些是反向题?
哪些是人口学变量?
哪些 Likert 题应该先形成维度得分?
这事让 LLM 靠列名猜,基本不稳。
所以后来加了数据理解层,先做确定性识别:
- 识别 Likert 量表候选。
- 识别人群属性变量。
- 识别可能的反向题。
- 识别题号前缀形成的题组。
- 生成 readiness hints。
然后再通过结构化澄清卡让用户确认。
这里也踩过一个体验坑:如果用户已经说了“做信度分析”或者点名了 V1-V10,Agent 还在那机械追问“请告诉我研究目标”,用户会很烦。
所以聊天 prompt 后来加了一条“已知意图就别再追问”:
- 用户点名统计方法。
- 用户点名变量范围。
- 用户问“这数据怎么分析”。
满足这些就先看数据结构,直接给具体建议,不要再套模板问基础问题。
这个改动看似小,其实是从“AI 助手味”变成“真正在干活”的关键。
第八阶段:版本化方案是被覆盖问题逼出来的
只要方案可以被用户改、被 AI 改、被分析页保存回方案,就一定会遇到覆盖问题。
用户打开的是 revision 3,AI 在后台基于 revision 3 生成一个 proposal;这时用户手动应用了另一个 proposal,方案变成 revision 4。前一个 proposal 如果还能直接应用,就会把新改动覆盖掉。
所以现在方案不是一段文本,而是一条 revision 链。
所有修改都走 proposal:
- 手工 proposal
- AI proposal
- 分析页保存回方案 proposal
- 恢复旧版本
- undo
应用 proposal 一定创建新 revision,不原地覆盖。
如果 base_revision 不匹配,就返回冲突。
这套机制写起来麻烦,但没有它,Agent 就会变成“越帮越乱”的源头。
第九阶段:局部 diff 比整套重生成更重要
用户说“把第二步变量换一下”,最危险的做法是让 AI 重新生成整套方案。
因为它可能顺手把别的步骤也改了。
所以现在的规则是:
- 默认局部补丁。
- 完整重生成必须是独立按钮,还要二次确认。
- 改说明,不撤销确认。
- 改方法、变量、参数,只让当前步骤回到待确认。
- 改输出或依赖,当前和受影响下游回到待确认。
- 删除被依赖步骤,必须显式处理下游。
这很像写代码里的 patch。
用户不是让 AI “重写整个项目”,只是让 AI “改这一段”。
Agent 要像 Codex,不是因为它会聊天,而是因为它知道自己在改哪个对象、产生了什么 diff、影响哪些下游。
第十阶段:执行不是 for 循环,是 DAG
一套分析方案可能不是简单顺序执行。
有些步骤只是顺序依赖,有些步骤依赖上一步产生的新数据版本,有些步骤输出的得分列要给后续回归或 SEM 用。
所以执行层做成了 DAG 调度:
- 每一步有 step_id。
- 每一步有 dependencies。
- 每一步有输入数据版本。
- 参数会解析上游输出引用。
- 运行时生成 config_fingerprint。
- 同配置、同输入版本的试跑结果可以复用。
- 单步失败时,下游标记 blocked,不一定拖死所有独立分支。
这里还有一个细节:Agent 方案运行里产生的新数据版本,不应该抢占用户当前正在看的数据版本。
普通分析 tab 跑方法,可以激活新数据版本;Agent DAG 中间步骤产生的新版本属于步骤血缘,不能突然把用户工作台数据切走。
这种细节很不显眼,但它决定用户会不会觉得系统“自己乱跳”。
第十一阶段:报告也不能让 AI 随便写
报告生成同样不能黑箱。
现在报告只能引用当前锁定方案运行出来的 artifact。
每张表、每张图都有稳定 artifact_id。AI 可以推荐哪些纳入报告,可以写段落,可以重写章节,但不能凭空引用方案外结果,也不能篡改只读表图。
这也是白箱原则的一部分:
报告不是“AI 看着结果自由发挥写一篇”,而是“基于用户确认的 artifact 组织成文档”。
否则最后用户问“这个结论来自哪张表”,系统答不上来。
第十二阶段:前端比想象中麻烦
Agent 前端不是一个聊天框。
它同时要承接:
- 聊天消息
- 澄清卡
- 方案卡
- proposal diff
- revision 历史
- 锁定状态
- planning job
- run 状态
- batch 进度
- artifact 目录
- 报告状态
- 结构化错误 details
所以前端最后变成一个工作流中心,而不是一个“AI 对话组件”。
最典型的细节是校验失败。
后端可能已经把 status 推到 needs_fix 并写了 requirements_issues_json,但前端刷新还没回来。为了用户立刻看到具体问题,前端还要临时保存接口异常里的 details,先给 IssuesCard 展示。
这类东西产品介绍不会写,但实际开发里全是这种缝。
还有会话列表同步。
最早消息内容已经落后端,但左侧对话列表还在 localStorage。换设备后,消息还在,列表没了。这个体验非常割裂。
后面又规划把标题、置顶、顺序、最后活跃时间同步到后端,localStorage 只做缓存和离线降级。
你以为你在写 Agent,写着写着就在补会话系统。
技术栈梳理
后端:
- Python
- FastAPI
- AioMySQL / MySQL
- Alembic
- Celery / Redis
- pandas / numpy
- scipy / statsmodels / pingouin / factor_analyzer / scikit-learn
- Rscript + jsonlite + lavaan
- DeepSeek / OpenAI-compatible Chat Completions
- AgentScope 2.0 用于对话 Agent 方向的能力探索和部分聊天工具层
前端:
- Vue 3
- Vite
- Pinia
- ag-grid
- marked / highlight.js / KaTeX / DOMPurify
- xlsx
核心工程模块:
- 方法注册表:
METHOD_REGISTRY/METHOD_META - 方案生成:requirements summary + funnel select/fill
- 硬校验:registry validator + intent validator
- 工作流:plan revision / proposal / lock / restore / undo
- 执行:DAG run service + execute_method job
- 报告:artifact catalog + outline + block document + freeze
- 排障:LLM failure traces + plan generation traces
我扒到的材料,按演化顺序整理
1. 早期 AgentScope 对话式方案
本地 .trae/documents/ai-agent-conversational-analysis.md 里,最早的方向是 AgentScope 2.0:
- ReActAgent
- 流式事件
- Permission system
- Toolkit
- 多轮记忆
- 浮动弹窗 + 独立 Tab 双入口
当时的判断是“不写 Agent 循环,不写事件系统,不写工具框架,不写权限控制”。
后来真实代码说明:框架可以省掉一部分聊天工程,但统计分析的状态机、省不了。
2. 后来把工具边界收窄
backend/agent/tools.py 现在的定位很明确:
- 只读数据
- 只读问卷
- 读参考资料
- 记录研究目标
- 记录假设
- 记录变量映射
不再把“生成方案 / 执行分析”作为 AgentScope 工具暴露。
这就是一次路线收缩:让 Agent 从“执行者”退回“需求整理员”。
3. Prompt 里开始充满“严禁”
backend/agent/service.py 的系统提示词非常长,而且很多话都不是写给用户看的,是被坑逼出来的边界:
- 严禁泄露思考过程。
- 严禁说“我已经生成方案”。
- 严禁说“分析已开始”。
- 严禁对用户暴露
col_3这种内部列名。 - 用户已知意图时别再基础追问。
- 结构化澄清卡已经覆盖的问题,chat 里不要重复问。
这类 prompt 很像产品现场补丁:每一条都是某次输出翻车后的止血。
4. AGENTS 规则把“白箱不可让步”写成底线
项目规则里明确写了智能分析是招牌差异点:
- 同一份数据 + 同一份方案,任何时候执行,结果必须复现。
- 用户明确指定方法,LLM 必须直接照办。
- 执行能力只能来自
METHOD_REGISTRY。 - 方案必须先生成 summary,再 registry 校验。
- 坏方案不能漏到执行流。
- 禁止 LLM 现写 Python、动态 import、eval/exec、沙箱、远程模型代跑。
这已经不是建议,是产品底线。
5. trace 规划文档是一次排障复盘
.trae/specs/trace-plan-generation/spec.md 里写得很直接:
- 用户反馈步骤 4 方案规划反复失败。
- 错误信息只有空的原文片段。
- 现有日志无法回答“这次规划到底跑了哪些 LLM 调用”。
所以补了:
agent_plan_generation_traces- 每次规划生成
trace_id - 每次 LLM 调用存完整 prompt / response
- parse_status 区分 success / empty / parse_error
- 管理员接口查询最近一次规划 trace
这是典型的“被线上问题逼出来的工程化”。
6. 工作流 Codex 化文档说明目标已经变了
backend/docs/AGENT_WORKFLOW_CODEX_PLAN.md 的目标不是做通用 Agent,而是做“统计分析白盒协作流”:
- 上下文连续
- 对象可操作
- 差异可见
- 状态透明
- 结果可复用
这句话很关键。
它说明这个 Agent 最后追求的不是“像人一样聊天”,而是“像 Codex 一样改对象”。
7. 追踪矩阵把坑变成验收项
AGENT_WORKFLOW_TRACEABILITY_MATRIX.md 把很多坑都变成了可验收规则:
- AI 不得静默换方法。
- 局部修改不能整份覆盖。
- 修改说明不能误伤确认状态。
- 修改变量只重置当前步骤确认。
- 删除被依赖步骤必须显式处理下游。
- 方案图拒绝环。
- 运行指纹稳定,支持复用。
- 报告只能基于 artifact。
- 对话与方案来源要可追溯。
这就是从“我觉得应该这样”变成“测试和验收要证明这样”。
8. 测试文件反过来暴露真实坑
测试里能看到很多真实事故的影子:
- 截断 JSON 要重试。
- 空 Markdown 围栏不能再抛 char 0。
- 中文标点要修复。
- 尾逗号要修复。
- AI 没明确收到换方法指令时,不能把 Pearson 改 Spearman。
- 校验失败要带结构化 details。
- HTTP 错误没 details 时还要兼容老前端。
测试名比产品文档更诚实。
坑位总表
| 坑 | 最开始以为 | 实际发生 | 后来怎么补 |
|---|---|---|---|
| AgentScope 一接就完事 | 框架托管 ReAct 和工具,省状态机 | 统计分析流程必须可审计,不能交给 Agent 自由跑 | Agent 只做聊天和记笔记,生成 / 执行走工作流接口 |
| 用户方法被 AI 替换 | Prompt 说别乱换就行 | AI 会因为假设不满足偷偷改方法 | 后端 guard + intent validator,没明确要求就拒绝换 method_key |
| 一次性输出完整方案 | 大 prompt 信息全,效果应该好 | JSON 截断、空壳、尾部断、变量映射挤爆输出 | funnel:大类选择、方法选择、骨架、单方法填充 |
| LLM 报错难查 | 看日志就行 | 日志只有 raw 头部,甚至原文片段为空 | trace_id + 每次 LLM 调用完整落库 |
| 列名直接给用户看 | 内部列名短,好处理 | 用户看 col_3 完全不知道是哪题 | alias / label 分离,出口翻译 |
| 量表题组让 LLM 猜 | LLM 看列名应该能懂 | 信度/EFA 题组一猜错,整个方案都歪 | 数据理解层 + 澄清卡 + 用户确认 |
| 校验失败给一句话 | 用户知道失败就行 | 用户不知道哪一步、哪个 slot、哪列错 | 结构化 details + IssuesCard |
| 修改方案直接覆盖 | 用户只是想改方案 | 多个 proposal 并发会互相覆盖 | revision 链 + base_revision 冲突 |
| 分析执行 for 循环 | 顺序跑每一步 | 有数据依赖、输出引用、版本血缘、结果复用 | DAG run service + fingerprint |
| 报告 AI 自由写 | 模型写报告更自然 | 结论可能引用方案外结果 | artifact catalog,报告只能引用当前方案结果 |
| localStorage 保存会话列表 | 本地体验快 | 换设备消息在,列表不在 | 后端同步标题、置顶、顺序、活跃时间 |
| thinking 模型更聪明 | 推理模型应该更强 | content 空、token 被思考吃掉、JSON 不在正文 | reasoning_content 兜底、max_tokens 拉高、提示切非 thinking |