先说结论:我不是反框架派 。LangChain 是我上手 Agent 开发用得最多的框架,工具注册一个 @tool 就完事,AgentExecutor 把整个 ReAct loop 封在里面,两周的活两天能干完。这个速度优势是真实的,不是幻觉。
但去年到现在,我在项目里做了一次「反向迁移」------把 Agent 的核心循环从框架里拆出来,自己手写。这篇文章讲清楚为什么,以及边界划在哪。
不是框架不好,是它在某些阶段会变成你的负担,而那时候代价已经产生了。
一、痛点是分阶段出现的,不是一开始就有
这点很关键。很多人一上来就说「框架有坑」,但如果你在做 POC,框架几乎没有副作用。
我把项目走下来,框架的问题是在四个不同阶段逐渐浮出来的:
探索期:真的很爽。 你在做 POC,目标是跑通流程,不是优化。框架省掉大量样板代码,几十行搭起一个能用的 Agent。
第一个奇怪的 bug 出现之后:感觉变了。 Agent 在某个特定场景下输出了错误的工具参数,你开始排查。你写的代码只有五十行,但报错的 stack trace 有四十层。往下追,追到框架内部。
这时候你面临一个很尴尬的问题:你不知道问题出在哪。
- 是你写的那五十行?
- 是框架某个版本的逻辑变了?
- 还是 callback 的触发时机?
你开始在 GitHub issue 里翻,或者一层层读框架源码。
打个比方:老式车出了故障,打开引擎盖,你能看见哪根管子漏油;现代豪华车出了故障,你打开引擎盖看到的是一堆看不懂的电子设备,只能去 4S 店让诊断仪扫。
框架的抽象层太多,排查问题需要穿透那些你没写、也不完全懂的层。这是实实在在的认知负担。
版本升级踩坑:又一层痛苦。 线上跑了几个月,某次例行依赖升级,框架改了接口,代码直接报错。要么回滚,要么改到兼容------可能涉及十几处修改。LangChain 早期版本升级频率很高,breaking change 是常见的。把核心业务逻辑建立在第三方框架上,线上稳定性就绑在了别人的发布节奏上。
(顺带说一个刚发生的例子:LangChain 的 AgentExecutor 已经被官方标记废弃,推荐迁移到 LangGraph。这就是典型的、无法预先规避的破坏性变更。)
规模化之后:隐性开销开始显形。 你开始关心 Agent 的调用延迟,发现某步 LLM 调用本身很快,但总耗时超预期。profile 之后发现:框架内部每次调用都在做你根本不需要的事------序列化中间结果、触发一堆 callback、记详细日志。
这些逻辑是框架为了通用性 设计进去的。对你的具体场景没用,但每次调用都在跑。高流量下,这些隐性开销会变成真实可见的延迟和成本。
二、手搓到底强在哪:就三个字,「完全掌控」
说清框架的痛,手搓的价值就好理解了。
第一,链路透明,可观测性好。 每一行代码你都知道在干什么,可以在任意位置加日志、打断点、插监控,没有黑盒。线上出问题,靠日志复现故障是最快的路------链路越清晰,定位根因越快。这在生产环境里是真实的时间和成本。
第二,精确裁剪,没有多余开销。 你只写确实需要的逻辑,不带通用性包袱。工具调用、历史维护、错误重试,每一块都按你的场景实现,没有「为了兼容其他用法」而存在的冗余。性能敏感的场景里,优化空间完全在自己手里。
第三,稳定可控,不受框架升级影响。 你自己写的接口不会突然变,没有来自外部的 breaking change。依赖只剩底层 LLM SDK,相对稳定,可以长期跑。
这不是我一个人的偏方。Anthropic 在官方的 Agent 构建指南里明确建议:不要一上来就用框架,先用最少的抽象把核心逻辑跑通。 他们的大意是------框架的抽象层会让你离底层更远,一旦出问题,调试成本比你省下的开发时间还高。
这和我的实际感受完全一致:框架帮你快速搭起来的东西,往往也是后期最难排查的东西。
还有个类比我觉得很准:框架是「租房」,装修好直接住,方便,但结构改不了,房东随时可能调整政策;手搓是「自建」,建得慢,但所有结构你都熟悉。 框架给你省了搭建时间,但你对这个「房子」的控制权始终有限。
三、同一个需求,两种写法差在哪(代码对比)
光说理论不够直观,拿最常见的场景来对比:实现一个带工具调用的 Agent loop。
框架版(LangChain):
python
from langchain.agents import AgentExecutor, create_openai_tools_agent
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
# 整个 ReAct 循环、工具调用、消息维护,全在 executor 内部
result = executor.invoke({"input": "帮我查一下今天的天气"})
确实简洁,三四行跑起来。但你想知道「这次调用 LLM 返回了什么、工具怎么被选中的、消息列表什么顺序」时------得去读 AgentExecutor 源码,内部调用链可能十几层。
手搓版:
python
messages = [{"role": "system", "content": system_prompt}]
messages.append({"role": "user", "content": user_input})
for i in range(max_turns): # 手动控制最大轮次
response = client.chat.completions.create(
model="gpt-4", messages=messages, tools=tool_schemas
)
msg = response.choices[0].message
messages.append(msg) # 把 LLM 响应加入对话历史
# 没有工具调用,说明 LLM 认为任务完成了
if not msg.tool_calls:
break
# 有工具调用,逐个执行并把结果写回消息列表
for tc in msg.tool_calls:
result = execute_tool(tc.function.name, tc.function.arguments)
messages.append({
"role": "tool",
"tool_call_id": tc.id,
"content": result
})
# 这里可以随意加日志、监控、重试逻辑
logger.info(f"工具 {tc.function.name} 返回: {result}")
代码量多一些,但消息列表怎么拼的、工具怎么选的、循环什么时候退出,每个细节都摆在明面上。出了问题,看这二十行就够了。
这就是「完全掌控」的具体含义------不是说框架不好,而是当你需要理解和调试每一个环节时,手搓版本给你的确定性是框架给不了的。
四、那到底什么时候用框架?给你一张判断表
这不是非此即彼。判断的关键是:项目处在什么阶段,以及对控制权的要求有多高。
| 用框架 | 手搓 |
|---|---|
| POC 阶段快速验证 idea | 准备上生产,稳定性是核心关切 |
| 团队刚接触 Agent 开发 | 流量上来了,性能和成本敏感 |
| 周边工具(文档解析、向量检索)依赖框架生态 | 业务逻辑高度定制,和框架通用设计偏差大 |
| 核心逻辑本身复杂度不高 | 需要高可观测性,链路要能随时监控回溯 |
折中方案:核心手写,周边借用
实践中最常见也最务实的,是介于两者之间------核心逻辑手写,周边工具性功能借用框架。
控制边界这么划:
- 手写(Agent 的「心脏」):工具调用循环、对话历史管理、错误处理与重试、任务状态维护。这些直接决定系统行为,必须百分百理解、百分百掌控。
- 借用(工具性周边):LangSmith 的 tracing、LlamaIndex 的文档解析、某个向量库的 Python 客户端。这些出了问题一眼就能看出来,不会造成黑盒困境,用现成的完全值得。
就像盖房子:承重墙在哪、房间怎么布局,你必须自己掌控;但门锁、插座面板、水龙头,完全可以买现成的。
五、一个判断信号,比任何规则都好用
最后说一个我自己在用的判断法。
如果你能清楚说出「框架在某个地方替我做了什么、我调的这个方法内部发生了什么」------说明你理解它,用起来有掌控感。
如果你只是调了一个方法、但完全不知道里面发生了什么------出了问题它就是不透明黑盒,这才是需要警惕的信号。
真实的项目演进轨迹通常是这样的:先用框架快速跑通、验证方向;遇到第一批线上问题后,把排查困难的关键部分换成手写;流量上来后,把性能敏感的核心逻辑全部手写;最后,框架只保留那些确实做得好的周边工具。
框架本身不是问题,「不理解就依赖」才是。
本文内容整理自我的个人知识库(Java 后端 / 大模型应用 / AI 工程化三个方向)。如果你在做 Agent 开发时也踩过类似的坑,欢迎在评论区聊聊。