从 POC 到生产的裂缝
如今人人都在做 stateful agents。可有状态是读路径的属性 —— 不是存储的属性。两套 harness 通过了每一次 demo,却在生产环境里悄悄退化成了无状态。
Agent 系统里最贵的 bug 从来不是崩溃的那种,而是那些返回一个看似合理的值的 —— 一个空列表、一段格式完好的字符串 —— 让下游一切照常运行,仿佛系统真的在工作。harness 通过了 demo、上了线,只在几个月后的真实流量里才暴露出窟窿:表现为"这 agent 好像没有记忆",或者"工具能用,除了它不能用的时候"。
我审计过一套生产环境的 harness,它用一个 14B 参数的开源模型驱动一队专职子 agent。纸面上它什么都有:持久化上下文、工具层、supervisor 路由。但其中两根支柱是空心的 —— 不是缺失,是空心。围绕它们的脚手架足够完整,完整到没人察觉。下面把这两个都还原成通用的模式讲一遍 —— 因为如果你也围着一个小模型搭过 harness,你大概率至少踩中过其中一个。
这套 harness 有一个货真价实的持久化层。每一轮都把上下文写进存储 —— append 语义、版本冲突重试、身份校验、双写镜像一应俱全。一个 get_context_service 被注入到子 agent 的 十二 个调用点。按任何结构性指标衡量,记忆都是接好线的。
然后是读路径:get_context_messages()。那段把存储记录还原成模型消息的函数体,在过去某次重构里被注释掉了,替换成一句光秃秃的 return []。它唯一的调用方 —— 主轮次循环 —— 尽职地在每个请求开头拉取历史,拿回一个空列表。README 承诺"凭 contextId 恢复任意对话"。代码忠实地写下了那段历史,却一个字都没读回来过。
它为什么藏得这么深:每一个可观测的信号都是绿的。存储在填满。save 调用成功。十二个注入点都能解析。看板显示上下文已写入。那唯一被切断的一跳,是个没有副作用可供监控的纯函数变换 —— 而模型拿到一段空历史,就只是表现得像个无状态聊天机器人,读起来像"小模型本来就健忘",而不是"我们的读路径返回了一个字面意义上的空列表"。
2026 视角 整个生态如今正拿这根支柱当卖点 —— "universal memory layer"、"stateful agents"、"durable execution"。但一个只写不读的记忆层,是一份 write-only 日志;而有状态是读路径的属性。这不是缺了个功能,这是一场披着绿色看板的 durability failure(持久性失效)。
破绽在这儿
基础设施的广度不等于它的深度。十二个调用点接到一个服务,说明管道铺好了;至于水到底出不出来,它一个字也没说。在你的持久化层和工具层里 grep 一下 return []、return None、pass、TODO —— 尤其是那些蹲在 README 声称"能用"的函数体里的。
现代模型端点都提供原生函数调用:模型吐出一个结构化的 tool_calls 字段,运行时直接从里面读 name 和 arguments,分发毫不含糊。我审计的这套 harness 没用它。它反而提示模型打印一段带围栏的 JSON,再用 re.search(r'```json ... ```') 在原始文本上把调用抠回来 —— 而原生绑定工具只存在于一条生产永远走不到的 mock 路径里。
这在 demo 里能跑,是因为一个 14B 模型在好提示词下通常能把那段格式打对。"通常"正是问题所在。只要模型把 JSON 裹进一句话、吐出两段块、多带一个逗号,或者正则撞上嵌套花括号,解析就会悄无声息地落空 —— 而一次落空的解析,在下游看来和"模型选择了不调工具"一模一样。
2026 视角 到今天,这甚至不再是一道自建还是选型的题。整个生态已经把工具调用标准化成了一套协议 —— MCP,spec 版本 2025-11-25,主流框架悉数采纳 —— 正是为了让没人再去手搓模型↔工具这道边界。从散文里解析 JSON 不是绕过它的捷径,而是主动退出了所有人都已收敛到的那份唯一契约。
还有第二个、更微妙的理由偏向原生字段 —— 尽管它同样会坏:当它坏的时候,往往坏得很响。这就引出了它那位同门 ——
同门失效 · 同一道边界
原生工具调用也不是自动就安全的。在某些推理后端提供的 OpenAI 兼容端点上,流式 tool_calls 的续传 chunk 会把 name 和 id 显式重复成 null,而参考实现只是省略这些字段。一个写成 if (delta.name !== undefined) 的累加器,会让那个 null 覆盖掉首个 chunk 里抓到的真实名字 —— 于是每一次调用都变成 unknown tool ""。修法是 != null。这正是协议本该杀死的那类失效 —— 可它照样活了下来,因为标准规定的是数据的形状,管不住每个自称说这套协议的后端在 wire 上各自的漂移。和 Trap 02 是同一课,只是往下沉了一层:哪怕走原生路径,你和模型之间的这道边界,仍然是各家 provider 约定发生分歧、"通常没事"的解析去送命的地方。
为什么 2026 让这一切更锋利
Agent 的话题已经往前走了。卖点不再是"它能不能调工具",而是 stateful agents、durable execution、一个 universal memory layer。而这恰恰是这两个陷阱更重要、而非更不重要的原因:它们正是一套 harness 宣称拥有的属性,和它实际拥有的属性之间的那道缝。三个失效在 demo 里全都隐形,只有在 trajectory(轨迹)里才现形。
2025-11-25),好让没人再去手搓它。2026 年还从散文里正则抠工具调用,等于选了整个领域已一致同意抛弃的那条路。null,照样能溜过去 —— 契约救不了一个把它说得很随意的后端。为什么两个都能活到生产
两个陷阱共享同一种签名:一个结构上在场、功能上空心的组件,朝着一个看起来合法的值失效。空历史读起来像健忘。丢掉的工具调用读起来像模型的决定。两者都不抛异常,都不 page 任何人。而一次 demo —— 一个干净的轮次、一个格式良好的调用 —— 永远不会落在那个能暴露窟窿的输入上。"接好线"和"能工作"之间的距离,恰好就是你 demo 没试过的那批输入。
如果你不想自己手搓
当一套自建 harness 一次次滑进这些陷阱,一个有人维护的库 —— 或它底下的那套协议 —— 通常早已替你把边界处理的账付清了。下面是针对 7B–14B + 工具调用 + memory 技术栈的粗略定位。星数截至 2026 年 8 月;有几个一度处在中心的项目已明显停更,而有一份契约已成了其他所有人构建之上的默认底座。
| 项目 | Stars | 是什么 | 记忆模型 | 什么时候选它 |
|---|---|---|---|---|
| MCP 事实标准 | 89.8k | 不是 harness —— 而是模型↔工具这道边界的协议(spec 版本 2025-11-25)。servers 仓 89.8k,Python SDK 24.1k。 |
不适用(是传输层,不是记忆) | 改用这个 Trap 02 的直接答案。说这套契约,别再解析散文。 |
| LangGraph | 40.2k | 面向 agent 控制流的图运行时;create_react_agent 可跑在任意 OpenAI 兼容端点上。 |
Checkpointer(短期)+ store(长期,Postgres 支撑)。 | 生产级 控制流与状态边界最扎实;支持 durable execution。用于正经部署。 |
| Qwen-Agent 停更约 6 个月 | 17.0k | 阿里的 harness,自带 Qwen 原生函数调用模板;对接 vLLM。 | 历史累积 + 自动文件 RAG。 | 上手最快 你的模型栈就是 Qwen。胶水代码最少 —— 前提是你能接受它慢吞吞的发版节奏。 |
| Letta 前 MemGPT | 24.3k | 围绕可自我编辑的分层记忆构建的 agent server。 | core / archival / recall 三层,跨会话自动持久化。 | 记忆优先 长期记忆是硬需求,而不是一个附加功能。 |
| smolagents | 28.9k | 约千行的极简 harness;CodeAgent 把动作直接表达成代码。 |
轻量 / 自备实现。 | 原型 做实验、本地跑 transformers/Ollama、教学。 |
| mem0 | 63.8k | 是一个记忆层,不是 harness —— 把长期记忆栓到任意运行时上。 | 这就是它的全部工作;本地 embedding 可配。 | 组件 你想保留自己的 harness,但 Trap 01 一直咬你。 |
| gorilla / BFCL 停更约 4 个月 | 13.0k | 面向函数调用的训练 + 评测,不是运行时。 | 不适用 | 评测 用来挑哪个小模型工具调用调得好(BFCL 榜)。 |
| AutoGen 停更约 4 个月 | 60.6k | 多 agent 框架 —— 现已进入维护模式。 | 对话历史。 | 迁移风险 官方口径已指向继任项目。新项目:掂量一下。 |
| Open Interpreter | 68.1k | 已重写成一个 Rust 编码 CLI agent;不再是通用 harness。 | 会话内作用域。 | 已转型 你要的是那个 CLI 工具,而不是拿来搭东西的库。 |
| Pydantic AI 较新 | 19.4k | 工具调用端到端按类型化 schema 校验;一次解析要么符合、要么直接抛错。 | 消息历史;可插拔。 | 类型安全 如果 Trap 02 吓到你了:schema 就是契约,在边界处当场校验。 |
在你宣布"上线了"之前
name,当它为空或未知时告警。一次被丢弃或被抹空的调用应该 page 人,而不是混进"模型什么都没调"里。return []、return None、pass、TODO —— 尤其在那些本该干活的函数体里。null、被拆开的 arguments、被裹起来的块 —— 再把它们回放给你的解析器。各家 provider 的约定,恰恰在你的单测假设它们不会分歧的地方分歧。