13 · 不用框架怎么实现一个 Agent
我失业在家做求职作品集,第四周的任务是「做一个 Agent」。
打开教程,前十篇里有九篇告诉你:用 LangChain / LangGraph / LlamaIndex,三行代码接上,完事。
我没用。不是洁癖,是三个很具体的理由。这篇文章写我怎么手写的,以及——更重要的——什么时候你还是该用框架。
一、为什么不用:三个理由
1. 我要证明的是「我会做」,不是「我会调」
简历上写「熟练使用 LangChain」,面试官心里想的是「那换个人也能」。
手写一遍,我能说清楚每一步为什么这么写:为什么解析要做三态、为什么熔断设 2 次而不是 1 次、为什么观察要截断。这些是判断,不是配置。
框架把判断藏起来了 —— 你调create_react_agent(),它跑起来了,但你不知道它在哪一步做了什么取舍。
2. 框架的版本和抽象,会吃掉我的调试时间
LangChain 那套抽象(chain / runnable / agent executor)本身就要学。而我只有六周,其中一周要做 Agent + MCP + 可观测。
我要的是「Agent 的原理」,不是「某个框架的 API」。这两件事花的时间差不多,但前者可迁移,后者可能下个版本就变了。
3. 最实际的一条:不用框架,可观测才好做
这条是我做完才想明白的。
框架的 Agent 跑起来,你想看每一步发生了什么,得先学会它的回调体系、它的 trace 格式。而我自己写的循环,事件是我自己发的——想在哪埋点就在哪埋点,格式我自己定。
后来我接 trace 的时候,Agent 代码改了 0 行。就这一条,省下的时间够我再写两个模块。
二、怎么实现:一个纯文本协议的 ReAct
核心决定:完全不传tools/tool_choice给模型。
# app/react.pyrun_react(...)# 不传 tools,不传 tool_choice模型用纯文本表达意图:
思考:我需要先查一下天气。 行动:get_weather 参数:{"city": "北京"}我的代码负责:解析 → 执行 → 把结果回灌 → 让模型继续。
为什么不走原生 function calling?因为它依赖服务端支持。如果我降级到某家免费模型、而它恰好不支持,整个 Agent 就废了。纯文本协议是最差的模型也能跑的那条路。
代价是:格式全靠约定,模型随时可能不守约。所以有了下面这些设计。
设计一:解析做三态,不是二值
{"kind":"action"}# 要调工具{"kind":"final"}# 给出最终答案{"kind":"unknown"}# 没解析出有效标记unknown这一态是关键。二值解析遇到格式不对,只能抛异常或者当 final——前者让整个任务崩掉,后者会让用户收到一个莫名其妙的答案。
我的处理:unknown当成一次观察结果回灌给模型,附一段格式示范,让它自己改。
设计二:给两次机会,然后熔断
MAX_UNKNOWN_IN_A_ROW=2# app/react.py:76设 2 而不是 1:给模型一次「照着格式重来」的机会。设 1 太苛刻(一次口误就崩),设 3 太浪费(改不对的格式改三次也改不对)。
设计三:三个终止出口,且绝不编造
出口 1 模型给出「最终答案」 —— 正常结束 出口 2 步数到 max_steps(默认 6) —— 安全结束,由我控制 出口 3 连续 2 次格式不明 —— 安全结束后两个返回ok=False+空答案。
这点我想了很久。熔断的目标是「不烧钱 + 不骗人」,不是「一定要有个答案」。让模型在不知道的情况下硬编一个,比空手而归糟糕得多。
设计四:观察回灌要截断,而且要说明截断了
OBSERVATION_MAX_CHARS=1500# app/react.py:79截断会丢信息,所以必须标注:「已截断,原长 N 字。如需完整结果请缩小查询范围」。
不标注会怎样?模型会以为它看到的就是全部,然后基于残缺信息下结论。告诉模型它的视野有边界,它才有可能自己缩小查询范围。
三、踩的坑:一个差点让我误判的 bug
做完之后我接了可观测(把事件流翻成 span 树,画瀑布图)。跑起来一看:
step 1: 651ms step 2: 640ms step 3: 620ms step 4: 590ms ---------------- 合计: 244% ← 比整次任务还长一倍多四步加起来 244%,我第一反应是「系统变慢了」。
实际是:我的 ReAct 事件词汇里有step_start,没有step_end。
没有结束信号,每一步就只能靠下一个step_start来推断结束;最后一步更是要等到根 span 兜底关闭才结束。于是每一步的耗时都变成了「从它开始到全程结束」。
尺子坏了,我却在怀疑被测的东西。
修法:下一个step_start/final/stop到来时,关闭上一步。修完我加了条测试专门钉死这个行为——这类 bug 特别容易在改事件词汇时复发。
这个坑给我的教训跟第三周一模一样:你没法优化一个你测不准的东西。先修尺子。
四、框架我装了,但主代码不用
项目里装着 LangGraph,但主代码不用。
我把它做成了对照脚本:
python scripts/langgraph_compare.py --with-langgraph这样我既保住了「未依赖框架」这个卖点,又能说清楚两种实现差异在哪——而且我知道 LangGraph 的 checkpointer 适合什么场景。
「我评估过,然后选择了不用」和「我不会」,在面试官耳朵里是两回事。
五、什么时候你还是该用框架
诚实说,下面这些情况别手写:
| 场景 | 为什么 |
|---|---|
| 生产环境,要长期维护 | 手写意味着所有边界情况你自己扛。框架踩过的坑比你多 |
| 需要复杂的分支 / 循环 / 并行图 | LangGraph 的状态图 + 条件边是真好用,手写状态机是自虐 |
| 需要持久化、断点续跑 | checkpointer 这类基础设施,别自己造 |
| 团队协作 | 框架是公共词汇。你手写的协议,只有你自己看得懂 |
我自己这个项目的结论也一样:生产更该用 workflow(确定性流程),agent 只作展示和面试素材。
Anthropic 那篇《Building Effective Agents》给了三个「不该用 agent」的判据,我照着对了一遍:
- 路径可预测 → 用 workflow
- 一次调用能完成 → 直接调 LLM,别套 agent
- 错误成本高且不可恢复 → workflow + 人工检查点
根本原因就一句:agent 的错误会累积(errors compound)。十步里每步 95% 正确,端到端只剩 60%。
六、一句话
手写一遍 Agent,最大的收获不是那个 Agent,是我知道它在哪一步会出错。
这份清单我写进了docs/Agent失败模式与兜底方案.md,十二类,每类都有真实触发场景和兜底手段。
框架能帮你跑起来,但不会告诉你它会怎么坏。