【Agent失败复盘】从 ReAct 到白箱工作流:一个数据分析 Agent 被现实按着重构的过程

8 阅读19分钟
别人都在教你怎么写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:

  1. 先选分析大类。
  2. 再在大类里选候选方法。
  3. 再生成方案骨架。
  4. 最后每个方法单独填 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_3q2_1x1。这对程序很方便,对用户很崩溃。

用户看的是题面:“我对当前工作要求感到满意”。你跟他说 col_11 全部是 7 分,他根本不知道你在说哪一题。

所以后面加了列别名机制:

  • name 是系统内部短标识,给 slot 和后端校验用。
  • label 是题面或原始列名,给用户看。
  • LLM 在 slot 里只能填 name,不能填题面文本。
  • 出口展示时再把 alias 翻译回 label。

甚至 prompt 里专门写了:不准对用户念 col_3 这种内部编号。

这不是洁癖。

如果这里不管,前端会出现两种灾难:

  1. LLM 把题面长文本填进 slot,后端校验列名不存在。
  2. 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

以上是我本次的血泪复盘