目录
一、凌晨三点的告警
想象一个场景:凌晨三点,你被手机告警吵醒。线上的 AI Agent 突然不工作了,客户投诉堆积如山。你爬起来看日志,发现报错栈穿过 RunnableBinding → RunnableSerializable → AgentExecutor → ChatModel → BaseLLM 整整六层框架内部代码,真正的业务错误被包装得面目全非。
你翻开 LangChain 的 GitHub Issues,发现三天前的一次小版本升级改了 AgentExecutor 的默认行为——它开始"智能地"吞掉 ToolCall 的异常,静默返回一个空结果。文档没有写这个变更,Changelog 里只有一句含糊的 "improved error handling"。
这不是故事会。这是 2025-2026 年间无数个生产团队真实经历过的场景。
当你追求的是 "跑通 demo",LangChain 是天使;当你追求的是 "凌晨三点不报警",它可能变成噩梦。
二、先问对问题:你到底在选什么?
很多人把这个问题简化为"LangChain 好不好用"。但真正的选型逻辑不是"工具好不好",而是"我的业务场景需要什么"。让我们先明确你的约束条件:
- 稳定性优先级极高——金融业务、线上生产环境,一次故障可能意味着真金白银的损失
- 未来要升级/切换模型——今天是 GPT-4o,明天可能换 DeepSeek、通义千问或私有部署的开源模型
- ToolCall / Agent 链路——不是简单的聊天,而是有工具调用、多轮推理的复杂链路
- Java 后端背景团队——对 Python 生态不熟,对 Spring 那套"声明式 + 注解"的企业级开发更有体感
带着这些约束,我们来看两个方案的真实差距。
三、正面交锋:七个维度的深度对比
不讲故事,先上硬核对比。每一个维度都是实际项目中踩过的坑、流过的血。
| 维度 | 手搓原生代码 | LangChain |
|---|---|---|
| 稳定性 | 链路完全透明,异常全部自己接管。没有框架的隐式重试、吞异常、静默降级。出问题栈信息干净,排查线上故障快。 | 黑盒较多。内部有大量默认行为:自动重试、异常捕获后吞错、缓存、回调链。很多行为文档不全,版本升级极易出现隐性行为变更。 |
| 模型切换 | 自己抽象 LLMClient 接口,不同模型各写一个适配器。上层业务完全无感切换,新增模型只新增一个类。 | 也支持多模型,但 Chain/Agent 逻辑强绑定框架对象。换模型不仅换 client,部分 prompt、解析器也要适配框架 API。 |
| ToolCall / Agent | 完全掌控:是否调用工具、参数校验、最大轮次、超时、截断、状态持久化全部自己写。可和现有数据库、业务表无缝对接。 | 框架封装好的黑盒。想做定制校验、特殊业务拦截,需要继承重写大量内部类。版本一改,重写的代码直接失效。 |
| 升级维护 | 框架依赖极少,只依赖 http 客户端 + 模型官方 SDK。业务逻辑完全掌握在团队手里。 | 大版本破坏性变更频繁(0.1→0.2→0.3 很多 API 直接删除)。锁版本也会遇到依赖包冲突。 |
| 开发效率 | 前期慢:需自己实现输出解析、tool 调度、上下文截断、token 统计。写一套抽象层约 1-3 周。 | 原型极快,几行代码跑通 Agent。但业务越复杂,定制成本指数上升,后期改黑盒代价远大于手写。 |
| 可观测性 | 日志、埋点、metrics 全部自己定义,贴合公司监控体系。每一步请求、返回、tool 入参出参精准打点。 | 内置回调体系,但埋点格式固定。想对接公司内部监控需适配回调。很多内部异常不完整透出。 |
| 故障排查 | 链路清晰,出问题看自己代码即可。可以复现每一步请求报文。 | 报错栈嵌套多层框架内部代码,真实业务错误被包装。线上排错成本高。 |
看完这张表,你可能会说:"LangChain 也不是一无是处啊?" 没错。关键在于你用什么标准衡量它。
四、LangChain 的暗坑:那些文档没写的行为
如果你还在犹豫,下面这些是真实生产项目中踩过的坑,每一个都是"文档没写、代码没写、但线上会犯"的隐式行为:
坑 1:悄悄的消息截断
LangChain 内部会在某些情况下自动截断、合并消息。你的代码看不到这一步,但线上偶发输出异常——因为历史消息被"智能"删减了,上下文丢了关键信息。
坑 2:ToolCall 静默失败
不同模型厂商的 function calling 格式有微小差异。LangChain 的解析逻辑是框架内部封装的,遇到格式差异时不会报错,而是静默返回 None,你的 Agent 会"认为没有工具可调用"然后自己编一个答案。日志里什么都看不出来。
坑 3:版本升级,行为变更
同样的 prompt + 同样的模型,升级 LangChain 版本后返回结果行为发生变化。文档没写这个变更,Changelog 里只有一句 "improved error handling"。你的测试用例全绿,但线上输出变了。
坑 4:依赖树爆炸
pip install langchain拉进来上百个间接依赖。任何一个子依赖出 bug 或版本冲突,都可能影响你的系统。生产环境运维成本直线上升。
这些坑的共同特点是:它们不是 bug,是"设计如此"。LangChain 的设计哲学是"智能默认值"——帮你做决策。但当你需要为每一行报错负责时,"帮你做决策"等于"帮你埋地雷"。
五、手搓的底气:核心代码其实不到 100 行
"手搓"听起来很重,但 Agent 的核心循环其实非常简单。看一下这个极简实现:
# ============ 适配器层:切换模型只改这里 ============
from abc import ABC, abstractmethod
class LLMProvider(ABC):
"""统一接口:所有模型适配器实现这个接口"""
@abstractmethod
def chat(self, messages: list[dict], tools: list = None) -> dict:
"""返回统一结构:{content, tool_calls, usage, raw}"""
pass
class OpenAIProvider(LLMProvider):
def __init__(self, model="gpt-4o", api_key=None):
from openai import OpenAI
self.client = OpenAI(api_key=api_key)
self.model = model
def chat(self, messages, tools=None):
resp = self.client.chat.completions.create(
model=self.model, messages=messages, tools=tools
)
msg = resp.choices[0].message
return {"content": msg.content, "tool_calls": msg.tool_calls}
class DashScopeProvider(LLMProvider): # 通义千问
def __init__(self, model="qwen-max", api_key=None):
import dashscope; dashscope.api_key = api_key
self.model = model
def chat(self, messages, tools=None):
resp = dashscope.Generation.call(model=self.model, messages=messages)
return {"content": resp.output.text, "tool_calls": None}
# ============ Agent 核心循环:不到 50 行 ============
def run_agent(query: str, provider: LLMProvider, tools: list, max_iter=10):
messages = [{"role": "user", "content": query}]
for i in range(max_iter):
result = provider.chat(messages, tools)
if not result["tool_calls"]:
return result["content"] # 模型给出最终答案
# 执行工具调用
messages.append({"role": "assistant", "tool_calls": result["tool_calls"]})
for tc in result["tool_calls"]:
output = execute_tool(tc, tools) # 你的业务逻辑
messages.append({"role": "tool", "content": output})
return "达到最大轮次限制"
就这么简单。整个 Agent 核心循环不到 50 行,加上适配器层不到 100 行。你拥有每一行代码的完全控制权——最大轮次、超时、参数校验、状态持久化,全部你说了算。
对比一下:用 LangChain 实现同样的功能,你需要理解 AgentExecutor、Runnable、BaseTool、OutputParser 等十几个类的交互关系,而这些类的行为在不同版本间还会变化。
六、预留模型升级口子:适配器模式的威力
你提到"后面升级模型也要预留口子"。手搓的适配器模式天然解决这个问题:
flowchart TB
subgraph BizLayer["业务层(完全不变)"]
A["Agent 核心循环
run_agent()"]
B["工具调度
execute_tool()"]
end
subgraph AdapterLayer["适配器层(切换模型只改这里)"]
C["LLMProvider 接口
chat() 统一签名"]
end
subgraph Models["模型实现(可随时新增)"]
D1["OpenAIProvider
GPT-4o / GPT-4.1"]
D2["DashScopeProvider
通义千问"]
D3["DeepSeekProvider
DeepSeek-V3"]
D4["ClaudeProvider
Claude 3.5"]
D5["... 新增模型
只需加一个类"]
end
A --> C
B --> C
C --> D1
C --> D2
C --> D3
C --> D4
C -.-> D5
适配器架构:业务层与模型完全解耦,新增模型只需加一个 Provider 类
这个架构带来三个实际的好处:
- 新增模型:只需写一个
XXXProvider类实现chat()方法,业务代码一行不改 - A/B 测试:同一个业务同时调用新旧模型,对比输出质量,灰度切换
- 降级容灾:主模型挂了,配置中心一键切到备用模型适配器,上层业务零改动
LangChain 也能做多模型,但你会被框架对象"绑架"——切换模型时,Chain、Agent、OutputParser 都可能需要跟着适配,口子预留得不够干净。
七、折中方案:很多企业的实际做法
真实世界里,不是非黑即白。很多成熟团队的做法是混合模式:
生产环境:手搓内核
- 自己实现 Agent 循环、tool 调度
- 自己实现 LLM 适配器层
- 状态落业务数据库
- 日志/埋点对接公司监控
开发环境:用 LangChain
- 快速验证 prompt 效果
- 跑通 Agent 思路原型
- 参考源码逻辑,抄到自研内核
- 放在 dev 依赖,不进生产镜像
这是最务实的路线:用 LangChain 的"快"来加速验证,用手搓的"稳"来保障生产。验证通过的 prompt、解析逻辑、tool 定义,复制迁移到自研内核,不直接 import 框架代码。
LangChain 的定位应该是"开发期的脚手架",不是"运行时的内核"。
八、什么时候才考虑直接用 LangChain 上生产?
为了公平,以下场景是可以考虑直接用 LangChain 上生产的——但需要同时满足全部条件:
- 业务允许一定容错,非核心链路,允许偶发异常
- 几乎不需要深度定制 Agent/Tool 逻辑,全部使用框架默认行为
- 团队愿意锁死框架版本,永远不做大版本升级
- 业务迭代慢,不会频繁新增模型、新增工具
你们的情况——追求高稳定性、未来要升级切换模型、大概率金融业务——一条都不满足。
九、写在最后:技术选型的底层逻辑
说到底,技术选型的核心问题不是"LangChain 好不好",而是"谁为你生产环境的每一个异常负责"。
当你用框架时,是框架的维护者在帮你做决策。他们做的决策是面向"大多数场景"的,不是面向你的金融级稳定性要求的。当框架的"智能默认值"和你的业务需求冲突时,你要么忍受,要么深入框架源码去重写——而后者的成本,往往比一开始就自己写还高。
手搓 100 行核心代码,换来的是:
- 每一行报错你都能看懂——因为是你写的
- 每一次模型切换只需改一个类——因为适配器是你设计的
- 每一个默认行为都是你决定的——没有"智能"的惊喜
- 框架升级永远不会影响你——因为你只依赖官方 SDK
这 100 行代码的投入,换来的是凌晨三点可以安稳睡觉的底气。这可能是你今年做过的最划算的技术投资。
最后说一句有温度的话:选型没有对错,只有"是否匹配你的约束"。如果你的团队还在起步阶段,业务允许试错,用 LangChain 快速跑起来也完全合理。但如果你的系统已经有了用户、有了 SLA、有了凌晨三点的告警——请把手搓当作一个严肃的选项。
这不是对 LangChain 的否定,是对你业务的尊重。

306

被折叠的 条评论
为什么被折叠?



