☰
LangChain 1.0 + LangGraph 多Agent协作实战:从零搭建出行管家系统
2026/10/21 7:13:13 网站建设 项目流程

最近一直在折腾 LangChain 1.0 的多 Agent 编排,越做越觉得有意思。单个 Agent 的玩具 Demo 谁都会写,但要让几个 Agent 像同事一样分工协作、各管一条业务线,在真实场景里跑通,完全是另一回事。这篇文章想分享一个我压了很久的实践项目:用 LangChain 1.0 + LangGraph 实现一个 Agent-to-Agent 协作的“出行管家”,让负责携程、美团、滴滴的三个子 Agent 在主管 Agent 的调度下,一起完成一次从订票、订酒店、订餐到打车的完整行程安排。

这个场景特别适合讲多 Agent 协作,因为三个平台服务的业务边界非常清晰:携程管机酒,美团管吃喝,滴滴管出行。边界清晰意味着 Agent 的职责可以切分得很干净,不需要硬塞给一个大模型去一把抓。我会从头梳理清楚整体架构、平台工具层怎么封装、LangGraph 怎么编排、实际运行会踩哪些坑,最后给出一套可以直接复用的代码骨架。适合刚把单 Agent 流程跑通、想往多 Agent 协作方向深入的同学看,也适合正在评估 LangChain 1.0 落地方案的开发者参考。

1. 为什么这次我没继续用单Agent

1.1 单Agent在真实场景里的三个尴尬

先说说我为什么非要拆成多个 Agent。最初我也试过只用一个 Agent,把所有工具全部塞进去,让大模型自己判断该调用哪个。结果发现,工具数量一旦超过十个,问题就开始变得明显。

第一是 prompt 膨胀。每个工具的描述、参数、说明都拼进系统提示词里,上下文越堆越长,模型对每条工具的注意力被稀释,经常出现“该调打车工具却跑去查酒店”的迷惑行为。有一次甚至连续三次把美团订座工具当成滴滴叫车工具用,因为它俩参数都有个“目的地”字段,模型被诱导了。第二是决策回路不稳定。所有工具处于同一个命名空间,Agent 在复杂流程里反复试错,token 消耗成倍增长,最后还不一定能走到正确节点。第三是职责不清导致不敢干实事。单 Agent 面对“订机票、订酒店、约车、找餐厅”这种跨域任务时,很容易只输出一个建议清单,而不是真正去调用工具完成预订。因为它的 prompt 里塞了大量“只查询、不操作”的限制,模型变得过度谨慎。

1.2 Agent协作的三种常见玩法

后来我把思路改成多 Agent 协作。常见的有三种玩法。

第一种是中心化监督者模式(Supervisor)。一个主管 Agent 不直接操作业务工具,只负责听懂用户需求、拆解任务、决定把子任务派给哪个子 Agent,最后把各子 Agent 的结果汇总成完整答复。这是最稳妥、最容易排查问题的模式。

第二种是层级委派模式。主管下面还有组长,组长再管组员,类似公司的两级管理。适合任务树特别深的场景,但对普通项目来说层级太多会让链路变长、延迟变高,调试起来也更头疼。

第三种是点对点群聊模式。多个 Agent 之间没有固定中心,可以互相发消息、互相追问。设计上很灵活,但真实落地非常难,因为你很难控制对话会不会跑偏,也没有一个清晰的“最终拍板人”。

我这次选的是第一种监督者模式,原因很直接:携程、美团、滴滴三个域之间几乎没有依赖关系,各自独立执行,最后由主管汇总裁决。逻辑上清晰,代码上可控。

1.3 这次采用的监督者架构

整体架构分四层。最上层是用户入口,直接用自然语言提问。第二层是监督者 Agent,负责理解任务、拆解子任务、调度三个子 Agent、汇总结果。第三层是三个工作 Agent:携程 Agent 负责航班酒店,美团 Agent 负责餐饮订座,滴滴 Agent 负责打车出行。第四层是工具层,也就是把三个平台的接口能力封装成 LangChain Tool。

我给每个子 Agent 的边界画得很死:携程 Agent 看不到美团和滴滴的工具,美团 Agent 看不到其他两个域的工具。这样每个 Agent 的系统提示词很小、工具列表很短,模型决策压力小很多,实测工具选择准确率有明显提升。

状态设计上,我让所有 Agent 共享一个状态对象,但每个子 Agent 只能往自己的字段里写结果。比如ctrip_plan只允许携程 Agent 写,meituan_plan只允许美团 Agent 写,didi_plan只允许滴滴 Agent 写。这样既保证信息互通,又避免相互踩脏数据。

2. 工具层:把三个平台变成Agent能用的“双手”

2.1 LangChain 1.0里怎么封装工具

既然谈到工具层,就得先讲清楚 LangChain 1.0 里最常用的两种封装方式。第一种是@tool装饰器,适合快速把普通函数变成 Agent 可调用的工具;第二种是继承BaseTool写完整类,适合需要精细控制执行过程的工具。大部分场景用@tool就够了。

有个非常容易被忽视的细节:工具函数必须带完整类型注解,LangChain 依赖类型信息生成工具的 JSON Schema。如果你写city参数不标str,或者budget没标float,模型拿到的工具描述就是残缺的,很容易传错参数。我第一次封携程工具时就踩过这个坑,调用参数死活校验不过,报错说是pydantic解析失败,最后发现就是少了一个类型注解。

工具描述也有讲究。我总结的经验是:一句话说清楚“这个工具是干什么的、什么时候用、绝对不能干什么”。比如查询酒店的工具描述写成“根据城市、入住日期、离店日期、人数和预算查询可预订酒店,只返回查询结果,不执行预订,用户未明确要求订房时禁止调用”。把限制条件写进描述,比在系统提示词里反复强调管用得多。

2.2 携程域:航班与酒店查询预订

携程域我封装了四个工具:查酒店、订酒店、查航班、订航班。所有工具都走同一套数据协议。

from langchain_core.tools import tool @tool def query_hotels(city: str, check_in: str, check_out: str, guests: int, budget: float) -> list[dict]: """根据城市、入住日期、离店日期、入住人数和预算查询酒店,只返回查询结果,不执行预订。 参数说明: - city: 城市名称 - check_in: 入住日期,格式 YYYY-MM-DD - check_out: 离店日期,格式 YYYY-MM-DD - guests: 入住人数 - budget: 每晚预算上限(元) """ return hotel_provider.search( city=city, check_in=check_in, check_out=check_out, guests=guests, budget=budget, ) @tool def book_hotel(hotel_id: str, check_in: str, check_out: str, guests: int) -> dict: """按酒店 ID 执行预订,预订成功后返回订单号。任何确认支付动作前都必须先让用户确认。""" return hotel_provider.book( hotel_id=hotel_id, check_in=check_in, check_out=check_out, guests=guests, )

hotel_provider是底层的平台适配器,它对外部接口做了一层翻译,把协议转换成统一结构返回。这样一来,Agent 层永远只感知统一数据结构,不会因为平台接口字段变动而频繁改代码。飞行相关的query_flights和book_flight封装方式完全一致,只是参数换成出发城市、到达城市和日期。

2.3 美团域:餐饮检索与订座

美团域的定位是解决“到陌生城市吃什么、去哪吃”的问题,配套的还有订座动作。我封了三个工具:找餐厅、看评价摘要、订座。

找餐厅的返回值需要控制长度。真实的餐饮数据非常大,我不可能把几十家店的全部信息一股脑塞回给 Agent,那样 token 瞬间爆炸。我的做法是每条餐厅只保留五个字段:店名、人均、评分、距离、推荐菜标签。Agent 拿到这些字段足够做筛选决策,真正需要详细评价时才调用评价摘要工具。

订座工具我特意加了一个前置条件:只有用户明确表达“要订位”时才允许调用。很多次演示中,Agent 只是找了几家餐厅就开始自动订座,这种过度热情在实际系统里是会出事故的。我通过在工具描述里写明“本工具仅在用户明确要求预订座位时调用”,加上监督者 Agent 在派单时也强调了这一点,问题才基本被压住。

2.4 滴滴域:出行预估与叫车

滴滴域的边界最简单:预估车费和发起叫车。预估工具输入出发地、目的地和乘车时间,返回车型、预估价格、预计用时。叫车工具需要额外的乘客数和行李数信息。

有一件事必须提醒:叫车是会产生真实费用的动作,绝对不能交给模型自动执行。我在叫车工具描述里直接写了“本工具会产生实际费用,调用后系统将暂停等待用户确认,未获得确认前禁止执行”。同时我在外层流程里配合 LangGraph 的打断机制做了人工确认,这个后面会细讲。

2.5 没有企业开放接口时怎么做“真落地”演示

说到“可落地”,这里必须正视一个问题。携程、美团、滴滴都有开放平台,但普通个人开发者能拿到的数据范围和权限是极其有限的,很多关键接口需要企业资质和线下合同,并不是注册个账号就能调。所以“真正可落地”要有两个层面的理解。

如果你所在的团队具备平台接入资格,那技术层面其实不复杂:写一个 Adapter 层,把三个平台的 HTTP 接口包装成我上面那套统一工具即可,鉴权、签名、限流都是常规工作。但如果你只是个人开发者想跑通演示,我建议不要假装自己有真实接口,而是搭建一个本地 Mock 服务,用 FastAPI 模拟每个平台的关键接口,返回符合统一格式的假数据。这样整条链路、编排逻辑、状态流转、人工确认机制全都是真的,只有数据是假的。将来团队拿到真实接口后,只需要替换hotel_provider这类适配器的内部实现,Agent 层完全不用动。

这就是“可落地”的正解:先把流程和平台解耦,让上层专注于协作逻辑,底层随时可以切换到真实数据源。

3. 多Agent协作的LangGraph实现

3.1 环境准备

前面铺垫完,进入实现环节。我用的环境依赖如下:

pip install "langchain>=1.0.0" "langgraph>=1.0.0" "langchain-openai" "langgraph-supervisor" "pydantic>=2.7" "fastapi" "uvicorn"

LangChain 1.0 相比 0.x 最大的变化是 API 稳定性和生态整合。以前langchain-core、langchain、langgraph各自独立版本号,互相踩版本兼容问题的情况很常见。1.0 之后核心 API 收敛,langgraph成为官方推荐的编排层,多 Agent 相关组件有官方预置的langgraph-supervisor包可以直接用。我建议新项目直接站在 1.0 的 API 上写,不要再去适应 0.x 的旧接口。

3.2 三个Worker Agent的构建

每个 Worker 用create_react_agent构建,这是一个高层封装,由 LangGraph 内部管理工具循环调用的节点和判断逻辑,我们只需要提供模型、工具和专属提示词。

from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent model = ChatOpenAI(model="gpt-4.1", temperature=0) ctrip_agent = create_react_agent( model=model, tools=[query_hotels, book_hotel, query_flights, book_flight], prompt=( "你是负责携程业务的出行Agent。" "你只能使用航班和酒店相关工具,不要处理餐饮和打车需求。" "查询清楚后返回结构化结果:航班、酒店、价格、订单号。" "如果查询无结果,直接说明原因,不要编造数据。" ), name="ctrip_agent", ) meituan_agent = create_react_agent( model=model, tools=[search_restaurants, get_review_summary, book_table], prompt=( "你是负责美团业务的餐饮Agent。" "你只负责推荐餐厅、查看评价、预订座位。" "所有餐厅推荐必须基于工具返回的真实数据。" ), name="meituan_agent", ) didi_agent = create_react_agent( model=model, tools=[estimate_ride, call_ride], prompt=( "你是负责滴滴出行的打车Agent。" "你只负责车费预估和发起叫车。" "任何叫车动作完成后都要等待人工确认。" ), name="didi_agent", )

注意我给每个 Agent 的 prompt 里都加了“你只管你的业务域”这句话。这不是废话,是防止模型在多 Agent 上下文里产生越界行为。

3.3 Supervision编排:从任务拆解到结果汇总

多 Agent 只是节点,真正把节点串起来的是编排逻辑。我用监督者模式做了一个主控制器,让主管 Agent 决定执行顺序。

from typing import Annotated, TypedDict import operator from langchain_core.messages import AnyMessage from langgraph.graph import StateGraph, START, END class OverallState(TypedDict): user_input: str ctrip_plan: dict meituan_plan: dict didi_plan: dict messages: Annotated[list[AnyMessage], operator.add] # 这里可以使用 langgraph_supervisor 一行搭建 from langgraph_supervisor import create_supervisor supervisor = create_supervisor( agents=[ctrip_agent, meituan_agent, didi_agent], model=model, prompt=( "你是出行管家主管。你的职责:理解用户出行需求," "拆分成机票酒店、餐饮、打车三个子任务,分配给对应Agent。" "所有子Agent返回后,你负责汇总并生成完整出行方案。" ), state_schema=OverallState, output_mode="last_message", ) graph = supervisor.compile()

如果你不想依赖预置组件,想完全掌控每个节点,也可以用StateGraph手写:把主管 Agent 作为一个节点,三个 Worker 分别作为独立节点,用工具路由决定切换到哪个节点。手写的好处是每个跳转理由可视化,排查问题时能很清楚“主管为什么把任务派给了美团 Agent”。预置组件的优点是省事,几十行就能搭好。我个人的建议是:前期用预置组件快速跑通全流程,理解消息流转方式之后,再手写 StateGraph 做深度定制。

3.4 落单前的人工确认:human-in-the-loop

这次实践里我认为最有价值的一环,是加入人工确认机制。整个编排链路里,查询、推荐、预估都是安全的,但“订酒店、订餐厅、叫车”会触发真实服务行为,绝不能让模型单方面执行到底。

LangGraph 提供了打断机制。在编译图时,用interrupt_before指定哪些节点执行前需要暂停,程序会停下来等待人工确认。

from langgraph.checkpoint.memory import MemorySaver workflow = supervisor.compile( checkpointer=MemorySaver(), interrupt_before=["book_hotel_node", "book_table_node", "call_ride_node"], ) config = {"configurable": {"thread_id": "trip-001"}} result = workflow.invoke({"user_input": "帮我安排一次A市到B市的两日游"}, config=config) # 运行到这里会暂停,输出待确认的预订项 print("等待确认:", result["messages"][-1].content) # 用户确认后,用 Command(resume=...) 让流程继续 from langgraph.types import Command workflow.invoke(Command(resume="都确认,继续执行"), config=config)

加上这层机制后,整个系统才谈得上“可落地”。Agent 可以尽情规划、推荐、预估,但涉及真金白银的动作一律由人来拍板。这套机制在实际演示里非常加分,因为它展示了 AI 应用的边界意识。

3.5 一次完整运行实录

我实际跑过一次完整的链路,输入是:“这周末想去B市玩两天,周五晚上从A市出发,两个人,预算三千内,想吃当地特色菜。”

主管 Agent 做了三件事:第一,拆任务,把航班酒店拆给携程 Agent,本地特色餐饮拆给美团 Agent,从机场到酒店的接送拆给滴滴 Agent;第二,并行调度三个子 Agent,让它们分别查数据;第三,汇总结果并判断预算是否超出。最终输出了一版方案:周五晚上 A 市飞 B 市的航班、B 市市中心一家快捷酒店的周末两晚、一家本地评分最高的小吃店、机场到酒店的接机预估价格,总花费正好卡在预算线以内。

第一次跑通的时候,三个 Agent 确实都只调用了自己域内的工具,没有一个越界,返回的数据也都是结构化的。问题出现在后面的调试过程中,因为模型版本、提示词写法、工具描述措辞都会影响稳定性,所以接下来把这几天遇到的坑集中整理出来。

4. 我踩过的坑和排查思路

4.1 子Agent互相“甩锅”、不干活

第二版调试时,我发现主管 Agent 有时会把餐饮任务发给携程 Agent,携程 Agent 又没有餐饮工具,于是回一句“我无法处理该请求”,整个流程就卡住了。

排查下来,根因是主管 Agent 在判断“哪个子 Agent 负责哪块”时出现了偏差。光靠模型理解“美团的定位是本地生活”不够,还得把工具描述的语义边界再收窄。我最终在三个子 Agent 的名字前面加了前缀,比如“ctrip_agent(仅限航班与酒店)”“meituan_agent(仅限餐饮与订座)”“didi_agent(仅限打车)”,同时在监督者的 prompt 里把“餐饮需求绝不能派给携程 Agent”写成了明确规则。这一改,类似的派单错误基本消灭。

4.2 Agent陷入循环

有一次携程 Agent 反复调用同一个查询航班工具,连续五次没结果还在循环查,最终触发了递归上限。原因是工具没有返回“无结果”的明确信号,模型以为没查询成功,于是不断重试。

解决办法有两个方向。一个是在工具内部对返回结果做包装,如果没有符合条件的数据,就返回标准化的“TRIPLE_NO_RESULT”字符串,让模型能明确识别“查询已完成但无结果”。另一个是在create_react_agent构建时设置recursion_limit,给单次执行画一条安全线,避免无休止重试消耗 token。两个方向我都做了,现在同一轮里 Agent 最多重试两到三次就会放弃并如实上报。

4.3 工具返回JSON过大

有个痛点记忆深刻:美团 Agent 查询餐厅时,我最初把完整的商户列表直接返回给模型,一次返回几百家店,单次工具调用的 token 消耗直接破万,而且模型根本看不过来,决策质量反而下降了。

后来我统一做了结构化摘要:每条返回记录只保留 5 个核心字段,单次查询最多返回 10 条。这其实是产品思维的问题,工具层应该像 API 网关一样对下行数据做裁剪,而不是把所有家底都摊给模型。换成精简结构后,整体 token 消耗下降了大概六成,查询准确率也上去了。

4.4 多Agent状态互相覆盖

再一个典型问题是多 Agent 状态互相覆盖。最初我用一个共享的messages字段存储所有 Agent 的中间结果,结果后面执果的 Agent 会把前面 Agent 的痕迹覆盖掉,汇总时只剩最后一次执行的内容。

LangGraph 推荐的模式是用Annotated[list[AnyMessage], operator.add]对消息做累加而不是覆盖。同时,我给三个子 Agent 各自分配了独立的业务字段,ctrip_plan、meituan_plan、didi_plan互不干涉。主管 Agent 最后只读这三个字段,就能拿到全部子任务的完整结果。

4.5 从Mock切到真实接口

我前面建议个人开发者先用 Mock 服务惯例,但 Mock 和真实接口之间还是有一处差异要提醒:网络异常、限流、签名过期这些真实世界的问题,Mock 永远模拟不出来。所以我在适配器层额外包了一层重试机制和错误降级逻辑。任何一个平台接口调用失败,Agent 都能拿到清晰的错误信息,而不是直接抛一个堆栈让流程中断。甚至可以在适配器里做降级策略:携程查询失败时,用城市周边备选方案兜底;滴滴预估失败时,先给出地铁的替代交通方案。

4.6 常见问题速查表

现象可能原因调整建议
子 Agent 互相推诿职责边界不清给 Agent 命名加前缀,prompt 写明“你只管某域”
反复调用同一工具工具未返回明确空结果信号返回标准化空结果标识,设置 recursion_limit
一次返回太多内容工具未做数据裁剪结构化摘要,单次最多返回 10 条核心字段
汇总时缺少子任务结果状态被后续节点覆盖使用 operator.add 累加,独立字段隔离
自动执行了预订类动作缺少人工确认环节用 interrupt_before 挂起关键节点
真实接口调不通缺重试、限流、降级逻辑适配器内加重试机制和降级兜底

我个人实践下来的体会是,多 Agent 系统的复杂度不在单个 Agent 的模型选择,而在职责划分、状态管理和流程控制。Agent 本身只是“大脑”,真正决定项目是否可落地的是周围那套结构化的“骨架”:清晰的工具协议、可靠的编排流程、必要的安全护栏。只要这三块搭扎实,后续无论是替换底层模型还是接入更多平台,都只是插拔式的改动。

最后再分享一个小技巧:每次调整完 Prompt 或工具描述,我都会把上一次的完整执行链路输出存档,下次改动之后拿同一批测试用例反复回归对比。这种肉眼可见的 diff,比任何抽象评估指标都更能帮助你判断改动到底是变好还是变坏。多 Agent 系统的稳定性很大程度是靠这种持续回归磨出来的,没有捷径。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询