1. 为什么“智能体开发”不是写个函数调用就完事?——从一个被反复删改的 demo 说起
我第一次用 LangChain 写出能“自主思考”的 Agent 时,兴奋地发到技术群,结果被一位做工业智能体的老哥直接点破:“你这叫 Chain,不叫 Agent。它没目标,没记忆,没失败回滚,连 retry 都靠你手动 catch,算哪门子智能?”——那会儿我才意识到,市面上 70% 的所谓“Agent 入门教程”,其实教的是“如何把 LLM 当高级模板引擎用”。真正的 Agent 开发,核心不在“调哪个 API”,而在“怎么让模型在约束下持续决策、自我修正、达成目标”。LangChain 不是胶水框架,它是帮你把“目标-规划-执行-反思”这套人类决策闭环,翻译成机器可执行逻辑的编译器。
关键词Agent、LangChain、智能体开发,这三个词必须放在一起理解:Agent 是目标驱动的自治系统;LangChain 是目前最成熟、文档最全、生态最丰富的 Python 侧 Agent 编排框架;而“智能体开发”本身,是一套融合了 Prompt 工程、工具调度、状态管理、错误恢复和评估验证的完整工程实践。它既不是纯算法研究,也不是简单 API 调用,而是介于应用开发与系统设计之间的新工种。适合两类人:一是已有 Python Web 或数据处理经验,想快速切入 AI 应用层的开发者;二是业务方技术负责人,需要评估是否值得把现有流程重构为 Agent 驱动。本文不讲“Hello World”,只拆解我踩过坑、重写过三版、最终跑通生产级任务流的真实路径——从 LangChain v0.1.x 到 v0.2.x 的架构演进,为什么AgentExecutor必须配合Tool接口重写,以及那个被官方文档轻描淡写、却让 90% 新手卡住三天的intermediate_steps字段到底该怎么用。
2. LangChain 的 Agent 架构不是“开箱即用”,而是“开箱即重构”——理解它的三层抽象本质
很多人学 LangChain Agent 卡在第一步:照着文档跑通create_react_agent,发现它只能查天气、算数学,一加自己的数据库工具就报错。问题不在代码,而在没看清 LangChain 对 Agent 的分层抽象设计。它不是单个类,而是三层契约(Contract)的叠加:
2.1 第一层:Agent 类型契约 —— “你承诺按什么范式思考?”
LangChain 定义了四种标准 Agent 类型,每种对应一套固定的推理循环(Reasoning Loop):
- ReAct Agent:严格遵循“Thought → Action → Observation → Thought…”四步循环,强制模型输出结构化 action 标签。这是最可控、最适合调试的类型,也是所有入门教程默认选择。
- Plan-and-Execute Agent:先生成完整执行计划(Plan),再逐条执行。适合步骤明确、依赖关系强的任务(如“订机票+订酒店+查天气”)。
- OpenAI Functions Agent:利用 OpenAI 原生 function calling 能力,由模型直接决定调用哪个工具及参数。性能高但黑盒性强,调试困难。
- Self-Ask Agent:专为问答优化,先拆解问题为子问题,再并行检索。对知识库问答友好,但不适合多步骤操作。
提示:新手务必从 ReAct 入手。它的
Thought和Action输出格式是固定的 JSON Schema,你能清晰看到模型每一步的“思考痕迹”,这是 debug 的黄金线索。别一上来就用 OpenAI Functions,看似省事,实则把所有错误都藏在模型内部,等于放弃调试权。
2.2 第二层:Tool 接口契约 —— “你承诺怎么被安全调用?”
Tool 不是随便写个函数就能注册的。LangChain 要求每个 Tool 必须实现name、description、args_schema(Pydantic 模型)和run方法。这个设计极其关键:
name和description会被拼进 system prompt,模型靠它理解工具能力;args_schema不仅校验输入,更决定了模型生成的 action 参数格式——如果 schema 定义city: str,模型就必须输出"city": "北京",而不是"location": "Beijing";run方法必须返回字符串(或可转字符串的对象),因为 Observation 会原样喂给下一轮模型。
我曾因args_schema漏写Optional导致模型传None进来,run方法直接抛TypeError,而 AgentExecutor 默认吞掉异常,只返回空 Observation,整个流程静默失败。后来才明白:Tool 的健壮性,就是 Agent 的鲁棒性底线。
2.3 第三层:AgentExecutor 执行契约 —— “你承诺怎么处理失败与状态?”
AgentExecutor是 LangChain Agent 的“操作系统内核”。它不关心模型怎么想,只负责三件事:
- 调度:把模型输出解析为 Tool 调用指令;
- 执行:调用对应 Tool,捕获异常,生成 Observation;
- 终止判断:检查模型是否输出
Final Answer,或达到最大 step 数。
但它的默认行为有致命缺陷:不暴露中间状态,不提供失败重试钩子,不记录 step 级日志。这就是为什么你跑 demo 总是“成功或失败”,却不知道哪一步挂了。要真正掌控 Agent,必须重写AgentExecutor的_call方法,或使用agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)开启详细日志——但verbose=True只打印到 stdout,无法存档分析。生产环境必须自己封装一层,把intermediate_steps(每一步的(action, observation)元组列表)持久化到数据库,并在失败时自动触发 fallback 流程。
3. 从零搭建一个可调试、可监控、可复现的 ReAct Agent —— 实战拆解每一步的“为什么”
我们以一个真实需求为例:开发一个“客户投诉处理助手”,能自动查询 CRM 获取客户信息、调用邮件 API 发送安抚邮件、最后更新工单状态。这不是玩具 demo,而是要嵌入客服系统的生产级模块。下面是我实际落地的步骤,每一步都标注了“为什么这样选”。
3.1 环境与依赖:避开 v0.1.x 与 v0.2.x 的兼容陷阱
# LangChain v0.2.x 是重大重构版本,API 全面不兼容 v0.1.x # 但 v0.2.x 的 Agent 模块更稳定,文档更清晰,强烈推荐新项目直接用 v0.2.x pip install langchain==0.2.14 langchain-community==0.2.12 langchain-openai==0.1.22 # 注意:langchain-openai 是独立包,不是 langchain 的子模块! # 如果漏装,create_react_agent 会报 ModuleNotFoundError经验:不要用
pip install langchain[all]。它会安装所有可选依赖(包括 Redis、PostgreSQL 驱动),而你的 Agent 可能只需要 OpenAI 和 Requests。精准安装能避免依赖冲突,也方便 Docker 镜像瘦身。
3.2 定义 Tool:用 Pydantic 强约束,而非字符串拼接
from pydantic import BaseModel, Field from typing import Optional class CRMQueryInput(BaseModel): customer_id: str = Field(description="客户唯一标识符,如 CRM 中的 contact_id") fields: Optional[list[str]] = Field( default=["name", "phone", "last_complaint_date"], description="要查询的字段列表,支持 name, phone, email, complaint_history" ) def query_crm(customer_id: str, fields: list[str] = None) -> str: # 实际调用 CRM API 的逻辑 # 关键:必须返回字符串!Observation 是文本,不是 dict if not fields: fields = ["name", "phone"] return f"客户张三,电话138****1234,最近投诉日期:2024-05-20" # 注册 Tool:name 和 description 将进入 system prompt crm_tool = Tool( name="query_crm", description="查询客户CRM信息。输入客户ID和可选字段列表。", args_schema=CRMQueryInput, func=query_crm )为什么用 Pydantic?因为模型生成的 action 参数必须严格匹配
args_schema。如果定义customer_id: str,模型就不能传{"id": "123"},否则AgentExecutor解析失败,直接报ValidationError。字符串拼接的 Tool(如lambda x: requests.get(...))无法做此校验,错误会延迟到run方法里才暴露,debug 成本翻倍。
3.3 构建 Agent:选择 ReAct,禁用 streaming(初期调试必备)
from langchain_openai import ChatOpenAI from langchain.agents import create_react_agent, AgentExecutor from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder # 初始化 LLM:temperature=0 保证输出稳定,便于 debug llm = ChatOpenAI(model="gpt-4-turbo", temperature=0, max_tokens=1024) # 构建 Prompt:ReAct 的 system prompt 是固定的,但你可以追加业务约束 prompt = ChatPromptTemplate.from_messages([ ("system", """你是一个专业的客户投诉处理助手。请严格遵守以下规则: 1. 必须使用 Thought/Action/Observation/Final Answer 格式进行推理; 2. Action name 必须是已知工具名:{tool_names}; 3. Action input 必须是 JSON 对象,且 key 必须匹配 tool 的 args_schema; 4. 如果客户ID未知,必须先询问用户,不能猜测; 5. 发送邮件前,必须确认客户姓名和电话已获取。"""), ("human", "{input}"), MessagesPlaceholder("agent_scratchpad"), # 这是中间步骤占位符,必须保留 ]) # 创建 Agent:注意,create_react_agent 返回的是 Runnable,不是 AgentExecutor agent = create_react_agent(llm, tools=[crm_tool, email_tool, update_ticket_tool], prompt=prompt) # 创建 Executor:关键参数 verbose=True,否则看不到中间步骤 agent_executor = AgentExecutor( agent=agent, tools=[crm_tool, email_tool, update_ticket_tool], verbose=True, # 必开!这是 debug 生命线 handle_parsing_errors=True, # 自动处理模型输出格式错误,返回友好的 error message max_iterations=10 # 防止死循环 )为什么禁用 streaming?因为 ReAct 的推理是严格同步的:模型必须输出完整的
Thought → Action → Observation链,才能进入下一步。streaming 会把Thought:和Action:拆成多个 chunk,导致解析器崩溃。等 Agent 稳定后,再考虑用AsyncAgentExecutor做异步优化。
3.4 调试核心:读懂intermediate_steps—— 那个被文档忽略的黄金字段
运行agent_executor.invoke({"input": "处理客户ID为C1001的投诉"})后,返回结果中有个intermediate_steps字段,它才是真相:
[ (AgentAction( tool="query_crm", tool_input={"customer_id": "C1001"}, log="Thought: 我需要先查询客户C1001的信息...\nAction: query_crm\nAction Input: {'customer_id': 'C1001'}" ), "客户张三,电话138****1234,最近投诉日期:2024-05-20"), (AgentAction( tool="send_email", tool_input={"to": "zhangsan@xxx.com", "subject": "投诉处理进展", "body": "尊敬的张三..."}, log="Thought: 已获取客户信息,现在发送安抚邮件...\nAction: send_email\nAction Input: {'to': 'zhangsan@xxx.com', ...}" ), "邮件已发送至 zhangsan@xxx.com"), (AgentAction( tool="update_ticket", tool_input={"ticket_id": "T20240520001", "status": "resolved"}, log="Thought: 邮件已发送,现在更新工单状态为已解决...\nAction: update_ticket\nAction Input: {'ticket_id': 'T20240520001', 'status': 'resolved'}" ), "工单 T20240520001 状态已更新为 resolved") ]这个列表就是 Agent 的“思维日志”。每一项
(action, observation)对应一次模型决策和一次工具执行。如果你的 Agent 失败了,第一件事就是检查这个列表:
- 最后一项的
observation是不是Error: ...?说明工具执行失败;action的tool_input是否符合args_schema?比如传了{"id": "C1001"}但 schema 要求{"customer_id": "C1001"};log字段里的Thought是否合理?如果模型说“我需要查询客户信息”,却调用了send_email,说明 prompt 约束失效。
4. 生产级 Agent 的三大隐形门槛:状态持久化、错误熔断、效果评估
跑通 demo 只是起点。真正在业务中落地,必须跨过三道坎。这些内容官方文档几乎不提,却是我花两周时间踩坑填平的。
4.1 状态持久化:为什么 Agent 不能每次都是“全新大脑”?
ReAct Agent 默认无状态。每次调用invoke,它都从头开始思考。但现实业务中,一个投诉处理可能跨小时、跨天,中间需要保存上下文。LangChain 提供RunnableWithMessageHistory,但它只存 chat history,不存intermediate_steps。我们必须自己设计状态存储:
# 使用 Redis 存储 session 级状态 import redis r = redis.Redis(host='localhost', port=6379, db=0) def get_session_state(session_id: str) -> dict: data = r.hgetall(f"agent:state:{session_id}") return {k.decode(): json.loads(v.decode()) for k, v in data.items()} if data else {} def save_session_state(session_id: str, state: dict): r.hset(f"agent:state:{session_id}", mapping={k: json.dumps(v) for k, v in state.items()}) r.expire(f"agent:state:{session_id}", 3600) # 1小时过期 # 在 agent_executor.invoke 前,注入历史 steps history = get_session_state("sess_123") if history.get("intermediate_steps"): # 把历史 steps 注入到 prompt 的 agent_scratchpad 中 # 这需要自定义 prompt template,把 history 转为字符串 pass关键点:
intermediate_steps必须作为MessagesPlaceholder的一部分喂给模型,否则模型不知道之前做过什么。这要求你把[(action, obs), ...]转成符合 ReAct 格式的文本,例如:Thought: 我需要查询客户信息... Action: query_crm Action Input: {"customer_id": "C1001"} Observation: 客户张三,电话138****1234... Thought: 已获取信息,现在发送邮件...
4.2 错误熔断:当工具调用失败时,Agent 不能“硬刚到底”
默认AgentExecutor遇到工具异常,会返回Error: ...作为 Observation,然后模型继续推理。但现实中,CRM 查询超时、邮件服务不可用是常态。必须加入熔断:
from tenacity import retry, stop_after_attempt, wait_exponential class RobustCRMTool(Tool): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._retry_decorator = retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10) ) def run(self, *args, **kwargs): try: return self._retry_decorator(super().run)(*args, **kwargs) except Exception as e: # 记录错误到监控系统 logger.error(f"CRM Tool failed after 3 retries: {e}") return "CRM 服务暂时不可用,请稍后再试。" # 注册时用 RobustCRMTool 替代原 Tool crm_tool = RobustCRMTool(...)为什么不用
handle_parsing_errors=True?因为它只处理模型输出格式错误(如 JSON 解析失败),不处理工具执行异常。熔断必须在 Tool 层实现,这是责任边界——Agent 负责决策,Tool 负责执行可靠性。
4.3 效果评估:用agent_evals框架量化“智能”程度
“Agent 跑通了”不等于“好用”。我们用agent_evals(LangChain 官方评估框架)设计三个维度测试:
- 功能正确性:给定输入,是否调用正确的工具序列?用
ToolCallCorrectnessEvaluator比对intermediate_steps与黄金标准。 - 结果准确性:最终答案是否与人工标注一致?用
StringMatchEvaluator。 - 效率合理性:是否出现冗余步骤?比如查询 CRM 后又查了一次,用自定义脚本统计
intermediate_steps长度分布。
from langchain.evaluation import load_evaluator evaluator = load_evaluator("tool-call-correctness", tools=[crm_tool, email_tool]) result = evaluator.evaluate_strings( prediction=agent_executor.invoke({"input": "处理C1001投诉"})["intermediate_steps"], reference=[("query_crm", {"customer_id": "C1001"}), ("send_email", {...})] ) print(f"Tool Call Accuracy: {result.score}") # 输出 0.0 ~ 1.0经验:评估必须自动化。我们每天凌晨用 100 条真实工单测试 Agent,生成报告。当
Tool Call Accuracy低于 0.95,就触发告警,团队必须当天定位原因——是 prompt 不够清晰?还是工具 description 有歧义?数据驱动,才能持续优化。
5. LangChain vs LangGraph:不是“升级替代”,而是“场景分治”——何时该换船?
搜索热词里频繁出现langchain和langgraph的区别、harness架构(langchain+langgraph),说明很多人在纠结要不要上 LangGraph。我的结论很直接:LangChain 是“单线程决策流水线”,LangGraph 是“多线程状态机编排器”。它们解决的问题根本不同。
| 维度 | LangChain Agent | LangGraph |
|---|---|---|
| 核心模型 | ReAct / Plan-and-Execute 循环 | 状态图(State Graph) + 节点(Node) + 边(Edge) |
| 适用场景 | 单目标、线性流程(如:查信息→发邮件→更新状态) | 多目标、分支条件、循环等待(如:投诉处理中,若客户未回复,3小时后自动升级;若邮件退信,则切换短信通道) |
| 状态管理 | 依赖intermediate_steps和外部存储 | 内置State对象,节点间自动传递,支持StateSnapshot版本控制 |
| 调试难度 | 中等(看intermediate_steps) | 高(需理解图遍历、条件边触发逻辑) |
| 学习曲线 | 平缓(熟悉 ReAct 即可) | 陡峭(需掌握 async、graphviz 可视化、state schema 设计) |
我的实际选择:客服助手初期用 LangChain ReAct,因为需求明确、流程固定;当业务方提出“如果客户24小时未确认,自动转人工”时,我们才引入 LangGraph,把整个流程重构为:
START → query_crm → send_email → wait_for_reply → ↗ (timeout) → escalate_to_human ↘ (confirmed) → update_ticket → ENDLangGraph 的
ConditionalEdge让这种分支逻辑变得清晰。但代价是:所有 Tool 必须重写为 async 函数,Stateschema 设计要覆盖所有分支路径。所以,别盲目跟风 LangGraph,先问自己:你的业务流程,有没有非线性的、需要状态记忆的、多出口的决策点?没有,就老实用 LangChain。
6. 给新手的三条血泪建议:绕开我花两周才明白的坑
最后,分享三个文档不会写、但会让你少走一个月弯路的实操建议:
6.1 不要迷信create_react_agent的 prompt,必须自己重写 system prompt
官方提供的 ReAct prompt 是通用模板,但业务场景越垂直,越需要定制。比如客服场景,必须加入:
- 明确禁止行为:“不得虚构客户信息,不得猜测未查询到的数据”;
- 明确 fallback 规则:“若 CRM 查询失败,必须向用户说明,不得跳过此步”;
- 明确术语映射:“工单号 = ticket_id,不是 order_id”。
我最初直接用默认 prompt,结果模型在 CRM 查询失败时,直接伪造了一个手机号发邮件。后来在 system prompt 里加上“严禁虚构任何客户字段,若查询失败,必须返回‘CRM 服务不可用’并停止后续步骤”,问题立刻解决。
6.2intermediate_steps是你的“黑匣子”,但必须学会解析它
很多新手拿到intermediate_steps就懵了,因为它是个 tuple 列表,里面混着AgentAction和str。写个解析函数:
def parse_intermediate_steps(steps): """将 intermediate_steps 转为易读的 dict 列表""" result = [] for i, (action, observation) in enumerate(steps): result.append({ "step": i + 1, "tool": action.tool, "input": action.tool_input, "thought": action.log.split("Thought:")[1].split("Action:")[0].strip(), "observation": observation[:100] + "..." if len(observation) > 100 else observation }) return result # 使用 steps = agent_executor.invoke({"input": "..."})["intermediate_steps"] for s in parse_intermediate_steps(steps): print(f"Step {s['step']}: {s['thought']} → {s['tool']}({s['input']}) → {s['observation']}")这个函数让我能在 10 秒内定位问题:是模型想错了?还是工具输错了?还是 Observation 返回格式不对?比翻日志快十倍。
6.3 本地知识库问答 ≠ Agent,别混淆概念
搜索热词里大量出现langchain本地知识库问答,但这是 RAG(Retrieval-Augmented Generation),不是 Agent。RAG 是“检索+生成”,Agent 是“规划+执行”。两者可以结合(如 Agent 用 Tool 调用 RAG 检索),但绝不能等同。我见过太多团队把 RAG 当成 Agent 上线,结果用户问“帮我订一张去上海的机票”,系统只会返回一堆机票政策 PDF 片段——因为它没有“订票”这个 Tool,也没有“规划行程”的能力。记住:Agent 的灵魂是 Tool,不是 LLM。没有 Tool,就没有 Action,就没有 Agent。
我在实际项目中,把 RAG 封装成一个search_knowledge_baseTool,这样 Agent 在需要政策依据时,会主动调用它。这才是正确的融合方式。