1. 从"会调API"到"能交付项目":企业级Agent开发到底卡在哪
很多人学LangChain的经历都差不多:跟着教程跑通了几个Chain,会写PromptTemplate,能接OpenAI的接口,然后呢?然后就没有然后了。真到了公司要做一个能上线的智能体系统,突然发现完全不知道从哪下手——状态怎么管、多轮对话怎么持久化、工具调用失败了怎么重试、多个Agent之间怎么协作、权限怎么控制、日志怎么追踪。这些问题,没有一个能在"Hello World"级别的教程里找到答案。
我自己在这个方向上摸索了挺长时间,也带过几个从零起步的项目,最大的感受是:LangChain只是入场券,真正决定项目能不能落地的是LangGraph、MCP和Agent工程化这三块。这四个东西不是并列关系,而是层层递进的关系——LangChain解决"怎么调模型和工具",LangGraph解决"怎么编排复杂流程和状态",MCP解决"工具怎么标准化接入",Agent解决"怎么让系统自主决策并可靠执行"。
这篇内容我打算把这四个框架串起来讲,不是那种每个框架单独写个Demo的讲法,而是按照一个真实企业项目的演进路径来展开:从最简单的链式调用开始,遇到瓶颈,引入图编排,再遇到工具管理混乱的问题,引入MCP协议,最后把整个系统升级成多Agent协作架构。每一步都会说清楚"为什么要这么做"和"不这么做会怎样"。
适合谁看?如果你已经会用Python,了解基本的LLM调用,但没做过完整的Agent项目,这篇内容应该能帮你少走不少弯路。如果你已经做过一些Demo,但总觉得离生产环境差一口气,那第四、五章关于状态管理和MCP接入的部分可能对你更有用。零基础也能看,但需要你边看边动手,光读不练的话,这些框架的坑你是体会不到的。
2. LangChain的真实定位:它该干什么,不该干什么
2.1 为什么很多人用LangChain用错了方向
LangChain最大的问题不是它不好用,而是它的抽象层次太多了。你打开官方文档,看到Chain、Agent、Tool、Memory、Retriever、OutputParser一大堆概念,很容易陷入"我要把所有模块都用上"的误区。实际上在企业项目里,LangChain最核心的价值就三个:统一模型接口、Prompt管理、输出解析。其他的功能,要么用不上,要么有更好的替代方案。
我见过一个项目,团队花了两个月时间用LangChain的Agent模块搭了一套工具调用系统,结果发现调试极其困难,因为LangChain的AgentExecutor把中间步骤封装得太深了,出了问题根本不知道是哪一步挂的。后来他们换成了LangGraph重新做,同样的功能两周就搞定了,而且可观测性好得多。这个教训就是:LangChain适合做"组件",不适合做"编排"。
那LangChain具体该怎么用?我的建议是把它当成一个"适配层"来用。你需要调不同厂商的模型,用LangChain的ChatModel接口统一掉;你需要管理大量的Prompt模板,用LangChain的PromptTemplate来组织;你需要让模型输出结构化的JSON,用它的OutputParser来约束。这些是它做得好的地方,也是经过大量项目验证的稳定功能。
2.2 模型接口统一:别小看这一层的价值
企业项目里很少只用一个模型。可能主力用某个商业模型,但某些敏感场景要用本地部署的开源模型,还有些场景要用便宜的小模型来做意图分类。如果没有统一的接口层,你的代码里就会到处是if-else判断,换个模型要改几十个文件。
LangChain的ChatModel接口帮你解决了这个问题。你只需要针对接口编程,底层换模型的时候改一个配置就行。具体来说,初始化模型的时候可以这样写:
from langchain_openai import ChatOpenAI from langchain_community.chat_models import ChatOllama # 商业模型,用于复杂推理 primary_llm = ChatOpenAI( model="gpt-4o", temperature=0.1, max_retries=3, timeout=60 ) # 本地模型,用于敏感数据处理 local_llm = ChatOllama( model="qwen2.5:14b", temperature=0.1, base_url="http://localhost:11434" )这里有几个参数值得注意。temperature在企业场景里通常设得很低,0.1甚至0,因为你需要的是稳定可复现的输出,不是创意。max_retries一定要设,网络抖动是常态,不设重试的话线上会莫名其妙地报错。timeout也很关键,默认值有时候太短,复杂推理任务还没跑完就超时了。
注意:不同厂商的模型对temperature的敏感度不一样。有些模型在temperature=0的时候反而会出现重复输出的问题,建议在0.1到0.3之间做A/B测试,找到你具体场景下的最优值。
2.3 Prompt管理:别把提示词散落在代码里
新手最容易犯的错误就是把Prompt直接写在业务代码里,用字符串拼接。项目小的时候没问题,一旦Prompt超过20个,你就完全记不住哪个Prompt对应哪个功能,改了一个地方可能影响三个功能。
正确的做法是把Prompt集中管理,用LangChain的PromptTemplate或者ChatPromptTemplate来组织。我一般会按业务模块分文件存放,比如prompts/customer_service.py、prompts/data_analysis.py这样。每个Prompt都带上版本号和变更说明,方便回溯。
from langchain_core.prompts import ChatPromptTemplate intent_classify_prompt = ChatPromptTemplate.from_messages([ ("system", """你是一个意图分类器。根据用户输入,判断其意图类别。 可选类别:查询订单、退换货、产品咨询、投诉建议、其他。 只输出类别名称,不要输出任何其他内容。"""), ("human", "{user_input}") ]) # 使用时 chain = intent_classify_prompt | primary_llm result = chain.invoke({"user_input": "我上周买的鞋子还没到"})这种写法的好处是Prompt和代码分离,产品经理要改提示词的时候不需要动代码逻辑。而且ChatPromptTemplate支持few-shot示例的注入,你可以把历史对话作为示例动态加进去,提升分类准确率。
2.4 输出解析:让模型的输出可编程
模型输出的是自然语言,但你的下游系统需要的是结构化数据。这个转换过程如果靠正则表达式去抠,维护成本极高。LangChain的OutputParser就是干这个的,它能把模型的自由文本输出转成Python对象。
最常用的是PydanticOutputParser,你定义一个Pydantic模型,Parser会自动生成格式说明注入到Prompt里,然后把模型输出解析成对应的对象。如果模型输出格式不对,它还能自动重试。
from langchain_core.output_parsers import PydanticOutputParser from pydantic import BaseModel, Field class OrderInfo(BaseModel): order_id: str = Field(description="订单编号") status: str = Field(description="订单状态") estimated_delivery: str = Field(description="预计送达时间") parser = PydanticOutputParser(pydantic_object=OrderInfo) prompt = ChatPromptTemplate.from_messages([ ("system", "从用户描述中提取订单信息。\n{format_instructions}"), ("human", "{user_input}") ]).partial(format_instructions=parser.get_format_instructions()) chain = prompt | primary_llm | parser实测下来,加了format_instructions之后,模型输出格式的正确率能从60%左右提升到95%以上。剩下的5%靠Parser的自动重试机制基本能兜住。但要注意,重试是有成本的,每次重试都是一次完整的模型调用,所以Prompt里的格式说明要写得足够清楚,别指望重试来兜底。
3. LangGraph:当Chain不够用的时候,你需要一张图
3.1 为什么线性Chain撑不住真实业务
LangChain的Chain本质是一条直线:输入经过A,A的输出给B,B的输出给C,结束。但真实业务几乎不可能是直线的。用户问了一个问题,你可能需要先判断意图,根据意图走不同的处理分支;处理过程中可能需要调用外部工具,工具返回的结果可能触发重试;多轮对话还需要记住之前的状态。
这些需求用Chain来实现会非常别扭。你可能会写一堆if-else来手动控制流程,代码很快就变成了一团乱麻。LangGraph就是为解决这个问题而生的,它把工作流建模成一张有向图,节点是处理步骤,边是流转条件,状态在节点之间传递。
打个比方,Chain像是一条流水线,产品从一头进去,经过固定工序从另一头出来。LangGraph像是一个车间,有不同的工位和传送带,根据产品类型和检测结果,可以走不同的路线,有的还要回流返工。
3.2 状态设计:整个图运转的核心
用LangGraph的第一件事是定义State。State是一个字典或者TypedDict,它会在所有节点之间共享。每个节点读取State,做一些处理,然后返回要更新的字段。LangGraph会自动把更新合并到State里。
State的设计直接决定了你的图好不好维护。我的经验是:State里只放需要跨节点共享的数据,节点内部的临时变量不要放进去。另外,对于列表类型的字段,要明确是覆盖还是追加。LangGraph提供了Annotated和operator.add来支持追加语义。
from typing import TypedDict, Annotated import operator class AgentState(TypedDict): messages: Annotated[list, operator.add] # 对话历史,追加模式 current_intent: str # 当前意图,覆盖模式 tool_results: Annotated[list, operator.add] # 工具调用结果,追加模式 retry_count: int # 重试次数,覆盖模式 final_answer: str # 最终答案,覆盖模式这里messages用了operator.add,意味着每次节点返回新的消息列表时,LangGraph会把它追加到已有的列表后面,而不是替换掉。这个设计对多轮对话非常关键,你不需要手动去维护历史消息的拼接。
3.3 条件边与循环:让流程真正"活"起来
LangGraph最强大的地方是条件边(conditional edge)。你可以定义一个路由函数,根据当前State决定下一步走哪个节点。这让"根据意图分流"、"失败重试"、"人工审核介入"这些逻辑变得非常自然。
from langgraph.graph import StateGraph, END def route_by_intent(state: AgentState): intent = state["current_intent"] if intent == "查询订单": return "query_order" elif intent == "退换货": return "process_return" elif state["retry_count"] > 3: return "human_handoff" else: return "general_response" builder = StateGraph(AgentState) builder.add_node("classify", classify_intent) builder.add_node("query_order", handle_order_query) builder.add_node("process_return", handle_return) builder.add_node("general_response", handle_general) builder.add_node("human_handoff", escalate_to_human) builder.set_entry_point("classify") builder.add_conditional_edges("classify", route_by_intent) builder.add_edge("query_order", END) builder.add_edge("process_return", END) builder.add_edge("general_response", END) builder.add_edge("human_handoff", END) graph = builder.compile()这段代码定义了一个意图分类后分流的图。注意retry_count > 3那个分支,这就是一个保护机制——当系统反复失败时,自动转人工,避免无限循环消耗资源。这种"兜底逻辑"在企业项目里是必须的,我见过太多Demo级别的Agent因为缺少兜底,在生产环境里陷入死循环把Token烧光。
3.4 持久化与断点续跑:生产环境的刚需
LangGraph支持Checkpointer机制,可以把每一步的状态持久化到数据库。这意味着如果服务重启了,可以从上次中断的地方继续跑,而不是从头开始。对于长流程的任务(比如需要人工审核的审批流),这个功能是刚需。
from langgraph.checkpoint.sqlite import SqliteSaver memory = SqliteSaver.from_conn_string("checkpoints.db") graph = builder.compile(checkpointer=memory) # 每次调用带上thread_id,同一个thread_id共享状态 config = {"configurable": {"thread_id": "user_123_session_1"}} result = graph.invoke({"messages": [("human", "我的订单到哪了")]}, config)thread_id是会话标识,同一个用户的多轮对话用同一个thread_id,状态就能串起来。生产环境建议用PostgreSQL或者Redis做Checkpointer,SQLite只适合本地开发。另外要注意Checkpointer的数据量会随着对话轮次增长,需要定期归档或清理。
提示:如果你用的是分布式部署,多个实例共享同一个Checkpointer,要确保thread_id的分配是幂等的,否则会出现状态覆盖的问题。我一般用"用户ID+会话ID"的组合来生成thread_id,并在入口处做去重。
4. MCP协议:工具接入的标准化答案
4.1 工具管理的混乱现状
在没有MCP之前,给Agent接工具是一件很痛苦的事。每个工具都要写一套描述、参数定义、调用逻辑,而且不同框架之间的工具定义格式还不一样。你在LangChain里定义的工具,换到另一个框架就得重写。更麻烦的是,当工具有几十个的时候,怎么让模型准确选择正确的工具,成了一个玄学问题。
MCP(Model Context Protocol)的出现就是为了解决这个问题。它定义了一套标准协议,工具提供方按照协议暴露自己的能力,Agent按照协议去发现和调用工具。你可以把它理解成"AI领域的USB接口"——不管什么设备,只要符合USB标准,插上就能用。
4.2 MCP的三种能力:Tools、Resources、Prompts
MCP协议里定义了三种核心能力,理解这三者的区别很重要:
| 能力类型 | 作用 | 典型场景 | 控制方 |
|---|---|---|---|
| Tools | 可执行的操作 | 查数据库、发邮件、调API | 模型决定调用 |
| Resources | 可读取的数据 | 文件内容、数据库记录 | 应用决定加载 |
| Prompts | 预定义的提示模板 | 标准化任务模板 | 用户主动选择 |
Tools是模型主动调用的,比如用户问"帮我查一下库存",模型会决定调用query_inventory这个Tool。Resources是应用层主动加载的,比如系统启动时把产品目录加载进来作为上下文。Prompts是用户或者应用选择的模板,比如"帮我写一封投诉回复邮件"对应一个预定义的Prompt模板。
在实际项目中,Tools用得最多,Resources次之,Prompts用得相对少。但三者配合起来才能发挥MCP的全部价值。
4.3 接入MCP Server的实操步骤
假设你有一个内部的订单管理系统,想把它暴露成MCP工具给Agent用。你需要写一个MCP Server,定义好工具的名称、描述、参数schema,然后启动服务。Agent这边通过MCP Client去连接这个Server,自动发现可用的工具。
from mcp.server import Server from mcp.types import Tool, TextContent server = Server("order-system") @server.list_tools() async def list_tools(): return [ Tool( name="query_order", description="根据订单号查询订单状态和物流信息", inputSchema={ "type": "object", "properties": { "order_id": {"type": "string", "description": "订单编号"} }, "required": ["order_id"] } ), Tool( name="cancel_order", description="取消指定订单,仅在订单未发货时有效", inputSchema={ "type": "object", "properties": { "order_id": {"type": "string", "description": "订单编号"}, "reason": {"type": "string", "description": "取消原因"} }, "required": ["order_id"] } ) ] @server.call_tool() async def call_tool(name: str, arguments: dict): if name == "query_order": result = await order_service.query(arguments["order_id"]) return [TextContent(type="text", text=result)] elif name == "cancel_order": result = await order_service.cancel( arguments["order_id"], arguments.get("reason", "") ) return [TextContent(type="text", text=result)]Agent这边接入就很简单了:
from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client server_params = StdioServerParameters( command="python", args=["order_server.py"] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() tools = await session.list_tools() # 把tools转换成LangChain的Tool格式 langchain_tools = convert_mcp_tools(tools)4.4 工具描述怎么写,模型才选得准
MCP解决了工具接入的标准化问题,但工具选择准确率的问题还是得靠描述质量。我踩过的坑是:工具描述写得太简略,模型经常选错工具。比如有两个工具,一个叫get_user_info,一个叫get_user_profile,描述都写"获取用户信息",模型根本分不清。
好的工具描述应该包含三个要素:做什么、什么时候用、什么时候不用。比如:
Tool( name="get_user_basic_info", description="""获取用户的基本信息,包括姓名、手机号、注册时间。 适用于需要确认用户身份或展示用户基本资料的场景。 如果需要获取用户的订单历史或消费记录,请使用get_user_orders工具。""", ... )加上"什么时候不用"这一句之后,工具选择的准确率提升非常明显。另外,参数描述也要写清楚格式要求,比如日期是"YYYY-MM-DD"还是时间戳,不写清楚的话模型会瞎猜。
注意:工具数量超过20个之后,建议做分组或者分层。一次性把所有工具塞给模型,不仅消耗Token,还会降低选择准确率。我一般的做法是按业务域分组,先用一个轻量模型做工具域路由,再在域内做具体工具选择。
5. Agent工程化:从Demo到生产要跨过的坎
5.1 单Agent的能力边界在哪里
很多人对Agent的期待是"给它一个目标,它自己搞定一切"。实际做下来你会发现,单Agent在复杂任务上的表现远不如预期。原因很简单:上下文窗口有限,工具太多会干扰选择,长流程中容易丢失目标。
我做过一个测试,让单Agent处理一个需要5步以上工具调用的任务,成功率不到40%。失败的原因主要是两类:一是中间步骤的结果没有正确传递到下一步,二是模型在长上下文中"忘记"了最初的目标。这不是模型能力的问题,是架构的问题。
所以企业级项目里,多Agent协作几乎是必然选择。但多Agent不是简单地把任务拆给几个Agent就完事了,怎么拆、怎么通信、怎么汇总,每一步都有讲究。
5.2 多Agent协作的三种模式
根据我的实践经验,多Agent协作主要有三种模式,适用场景不同:
模式一:主管-工人模式(Supervisor-Worker)。一个主管Agent负责拆解任务和分配,多个工人Agent负责执行具体子任务。适合任务可以清晰拆解的场景,比如"分析这份报告"可以拆成"提取数据"、"生成图表"、"撰写结论"三个子任务。
模式二:流水线模式(Pipeline)。多个Agent按顺序处理,前一个的输出是后一个的输入。适合有明确阶段划分的场景,比如内容生产流水线:选题Agent → 大纲Agent → 撰写Agent → 审核Agent。
模式三:辩论模式(Debate)。多个Agent对同一问题给出不同答案,然后由一个仲裁Agent综合判断。适合需要多角度分析的场景,比如风险评估、方案评审。
在LangGraph里实现这些模式,核心是用好条件边和子图。主管-工人模式可以用条件边做任务分发,流水线模式用顺序边串联,辩论模式用并行节点加汇总节点。
from langgraph.graph import StateGraph, END def create_supervisor_graph(): builder = StateGraph(SupervisorState) builder.add_node("supervisor", supervisor_node) builder.add_node("data_worker", data_worker_node) builder.add_node("chart_worker", chart_worker_node) builder.add_node("report_worker", report_worker_node) builder.set_entry_point("supervisor") builder.add_conditional_edges( "supervisor", lambda s: s["next_worker"], { "data": "data_worker", "chart": "chart_worker", "report": "report_worker", "finish": END } ) # 每个worker完成后回到supervisor builder.add_edge("data_worker", "supervisor") builder.add_edge("chart_worker", "supervisor") builder.add_edge("report_worker", "supervisor") return builder.compile()这个结构里,supervisor节点负责决定下一步派给谁,worker完成后回到supervisor,由supervisor判断是继续派活还是结束。这种"中心辐射"的结构比让worker之间直接通信要好维护得多。
5.3 可观测性:出了问题你得知道是哪出的
Agent系统最让人头疼的就是调试。用户说"它回答错了",你去看日志,只看到一堆模型调用记录,根本不知道中间发生了什么。所以可观测性不是可选项,是必选项。
我一般会在三个层面做埋点:节点级别记录每个节点的输入输出和耗时,工具级别记录每次工具调用的参数和返回,模型级别记录Token消耗和延迟。这些数据汇总起来,才能定位问题。
LangGraph本身支持Streaming,可以实时输出每个节点的执行情况。配合LangSmith或者自建的追踪系统,能把整个执行链路可视化出来。我建议在开发阶段就把这套东西搭好,别等到上线出了问题再补,那时候你会非常痛苦。
async for event in graph.astream( {"messages": [("human", user_input)]}, config={"configurable": {"thread_id": session_id}} ): for node_name, node_output in event.items(): logger.info(f"Node: {node_name}", extra={ "input": node_output.get("input"), "output": node_output.get("output"), "timestamp": time.time() })5.4 成本控制:别让Agent变成烧钱机器
Agent系统跑起来之后,成本很容易失控。一次用户请求可能触发十几次模型调用,如果每次都用大模型,账单会非常难看。我的经验是做好三件事:
第一,模型分级。意图分类、参数提取这种简单任务用小模型,复杂推理才用大模型。实测下来,简单任务用小模型的效果差距不大,但成本能降80%以上。
第二,缓存。相同的输入直接返回缓存结果,尤其是那些高频的、确定性的查询。LangChain提供了CacheBackedEmbeddings和LLM缓存机制,配置一下就能用。
第三,限制循环次数。每个图都要设置最大迭代次数,超过就强制结束或者转人工。我见过一个项目因为没设限制,一个用户请求触发了200多次模型调用,单次成本超过10块钱。
# 设置递归限制 config = { "configurable": {"thread_id": session_id}, "recursion_limit": 25 # 最大节点执行次数 }recursion_limit是LangGraph提供的保护机制,超过这个次数会抛出异常。建议根据你的图复杂度设置,一般20到50之间比较合理。
6. 一个完整的企业级Agent项目是怎么搭起来的
6.1 项目结构:别把所有代码塞一个文件里
我见过太多Demo项目把所有代码写在一个main.py里,几百行堆在一起。这种代码自己看着都累,更别说维护了。企业级项目一定要有清晰的结构。我一般会这样组织:
project/ ├── config/ │ ├── settings.py # 配置管理 │ └── prompts/ # Prompt模板 ├── core/ │ ├── llm_factory.py # 模型工厂 │ ├── state.py # 状态定义 │ └── graph_builder.py # 图构建 ├── nodes/ │ ├── classify.py # 意图分类节点 │ ├── tools.py # 工具调用节点 │ └── response.py # 响应生成节点 ├── mcp_servers/ │ ├── order_server.py # 订单系统MCP Server │ └── knowledge_server.py # 知识库MCP Server ├── agents/ │ ├── supervisor.py # 主管Agent │ └── workers.py # 工人Agent ├── observability/ │ ├── logger.py # 日志 │ └── tracer.py # 追踪 └── main.py # 入口这个结构的好处是职责清晰,改Prompt不用动代码,加工具不用改图,换模型不用改业务逻辑。团队协作的时候,每个人负责的模块边界很清楚。
6.2 从需求到落地的完整流程
假设我们要做一个客服Agent系统,需求是:用户可以用自然语言咨询订单、退换货、产品问题,系统自动处理,处理不了的转人工。
第一步:定义状态。根据需求,State里需要包含:对话历史、用户ID、当前意图、订单信息、工具调用结果、是否需要转人工。
第二步:画流程图。入口是意图分类,分三个分支:订单查询、退换货、产品咨询。订单查询调MCP工具查订单,退换货走审批流程,产品咨询查知识库。每个分支处理完后生成回复,如果置信度低或者用户明确要求,转人工。
第三步:实现节点。每个节点是一个函数,接收State返回更新。意图分类节点用小模型,工具调用节点通过MCP Client调工具,回复生成节点用大模型。
第四步:组装图。用StateGraph把节点连起来,加条件边做分流,加Checkpointer做持久化,加recursion_limit做保护。
第五步:测试与调优。用真实对话数据测试,记录每个节点的准确率和耗时,针对薄弱环节优化Prompt或者换模型。
6.3 上线前必须检查的几件事
项目跑通和能上线是两回事。上线前我一般会检查这些:
- 异常处理:每个节点都要有try-except,工具调用失败要有降级方案,模型超时要有重试。
- 限流:入口处做用户级别的限流,防止单个用户刷爆系统。
- 日志脱敏:用户手机号、地址这些敏感信息在日志里要脱敏。
- 回滚方案:新版本上线要有快速回滚的能力,Prompt和模型配置都要能热切换。
- 压测:模拟并发请求,看看系统的瓶颈在哪。Agent系统通常是IO密集型的,瓶颈一般在模型调用和工具调用上。
提示:上线初期建议用灰度发布,先放10%的流量进来,观察一周再全量。Agent系统的行为不确定性比传统系统高,灰度能帮你发现很多测试环境发现不了的问题。
7. 几个我踩过的坑和对应的解法
7.1 状态污染:多轮对话里的隐形杀手
多轮对话最容易出的问题是状态污染。用户第一轮问了订单A,第二轮问订单B,如果State里的订单信息没有正确更新,系统可能还在用订单A的数据回答。这个问题的根源是State的更新逻辑不清晰。
我的解法是:每个节点返回State更新时,明确指定是覆盖还是追加。对于订单信息这种"当前上下文"的数据,用覆盖模式;对于对话历史这种"累积"的数据,用追加模式。另外,在意图分类节点里加一个"上下文重置"的判断,当用户切换话题时,清空上一轮的临时数据。
7.2 工具调用超时:用户等不起
工具调用超时是另一个高频问题。外部API响应慢,用户等了十几秒还没结果,体验极差。我的做法是设置分级超时:3秒内没返回就先给用户一个"正在查询"的提示,10秒还没返回就降级到备用方案(比如查缓存或者转人工)。
LangGraph的节点里可以用asyncio.wait_for来实现超时控制:
import asyncio async def call_tool_with_timeout(tool, args, timeout=10): try: return await asyncio.wait_for(tool.ainvoke(args), timeout=timeout) except asyncio.TimeoutError: return {"error": "工具调用超时", "fallback": True}7.3 Prompt漂移:模型升级后的意外
模型厂商升级模型版本是常事,但升级之后你的Prompt可能就不work了。我遇到过一次,模型升级后,原本稳定的输出格式突然变了,导致Parser大量报错。后来我养成了习惯:每次模型版本变更,先跑一遍回归测试集,确认关键指标没有下降再全量切换。
回归测试集不用很大,覆盖核心场景就行,一般50到100条测试用例足够。重点看三个指标:意图分类准确率、工具选择准确率、输出格式正确率。任何一个指标下降超过5%,就要重新调Prompt。
7.4 上下文窗口:不是越大越好
很多人觉得上下文窗口越大越好,恨不得把整个知识库都塞进去。实际测试下来,上下文太长反而会降低模型的表现,因为关键信息被淹没了。我的经验是:上下文里只放当前任务真正需要的信息,无关的历史对话和文档要主动裁剪。
具体做法是:在State里维护一个"当前任务相关上下文"的字段,每次节点更新时只把相关的信息加进去。历史对话超过一定轮次后,用摘要代替原文。这样既控制了Token消耗,又提升了模型的表现。
8. 写在最后:一些个人体会
这套东西我从头到尾搭过几遍,最大的体会是:框架是工具,架构是能力。LangChain、LangGraph、MCP这些框架会不断更新迭代,但底层的架构思维是通用的——怎么设计状态、怎么编排流程、怎么管理工具、怎么保证可靠性,这些想清楚了,换什么框架都能快速上手。
另外就是别追求一步到位。我见过一些团队一上来就想搭一个"全能Agent",结果做了半年还在调bug。正确的做法是从一个小场景切入,把单点跑通,再逐步扩展。先做一个能查订单的Agent,跑稳了再加退换货,再加产品咨询,每一步都验证过再往前走。
最后说一个容易被忽视的点:Agent系统的评估体系要提前建。不要等到上线了才想怎么衡量好坏。从第一天起就记录每次交互的完整链路,定义清楚什么是"成功"什么是"失败",积累测试集。这些东西在后期调优的时候价值巨大,没有它们你就是在盲调。
这个方向变化很快,今天好用的方案明天可能就有更好的替代。保持动手,保持测试,别光看文章不动手。很多坑只有自己踩过才知道有多深,别人告诉你的经验,你听的时候觉得懂了,真到用的时候还是会犯同样的错。