
婚礼策划师这个词放在近两年的智能体开发语境里是一个非常合适的 LangChain 多智能体练手项目。它不像客服机器人那样只有一个对话入口也不像 RAG 问答那样只需要一条检索链路而是天然包含预算、场地、宾客、流程四个彼此独立、又需要协调配合的子任务。本教程用 Python 实现一个“中配”版本四个专业智能体各管一段一个意图路由器决定问题交给谁处理工具层用本地 JSON 模拟数据最后用 Streamlit 打包成可聊天的 Web 界面。学完以后你能掌握 LangChain 智能体、LangGraph 工具调用、结构化输出路由以及 Streamlit 聊天组件这套完整链路。这里的“中配”指的是比单提示词演示复杂、比生产级多智能体系统简单的中间档位有多个智能体、有工具、有路由、有界面但数据量小、没有复杂状态机、没有消息队列。这个粒度适合理解多智能体的核心机制也方便后续扩展成真正可上线的系统。1. 先拆多智能体设计不要先写代码多智能体项目最容易犯的错误是一上来就画状态图、写路由函数结果连业务边界都没想清楚。婚礼策划这个场景正好适合用来训练“拆智能体”的思路。1.1 单提示词方案为什么撑不住这个场景如果只用一个大 Prompt 让模型处理婚礼策划会出现三个问题。第一工具调用链路会变得很长。一个完整婚礼策划问答可能要查场地、查供应商、算预算、排流程中间还会涉及宾客人数。所有工具放在同一个 ReAct 循环里模型在“选择哪个工具”上会摇摆整体成功率明显下降。第二上下文容易膨胀。为了让模型理解“怎么查场地、怎么算桌数、怎么排流程”要把所有规则和示例塞进系统提示词。当提示词超过一定长度后模型对关键指令的注意力会分散回答质量不稳定。第三排错困难。用户问一句“预算 20 万怎么分配”如果调用链里混着场地查询和流程生成日志里很难判断是哪一步出了问题。多智能体把领域拆开之后每个环节可以单独测试、单独调优。1.2 LangChain 和 LangGraph 在这个项目里分别负责什么LangChain 和 LangGraph 经常被放在一起讨论但职责并不重叠。LangChain 提供的是基础组件模型封装、Prompt 模板、工具定义装饰器、输出解析器、向量存储接入等。本项目中ChatOpenAI来自langchain-openaitool装饰器来自langchain_core.tools它们都属于 LangChain 生态。LangGraph 提供的是状态和编排能力。你可以用它画一个包含节点、边、条件路由的状态图也可以直接使用它的create_react_agent快速生成一个带工具调用循环的 Agent。本项目中每个专业智能体都用create_react_agent创建外层再用手写路由来决定调用哪个智能体。一句话总结LangChain 给零件LangGraph 给流程。多智能体不一定非要 LangGraph但只要你不手写 ReAct 循环用它是最省事的方式。1.3 中配架构1 个路由 4 个专业智能体本教程采用分层路由模式也叫 Orchestrator-Worker 模式。用户的问题先进意图路由器。路由器不做具体策划只判断这个问题属于哪个领域然后调用对应智能体。智能体内部是一个独立的 ReAct 循环可以调用自己专属的工具。最后把智能体的回答返回给页面。这种模式的好处是每个智能体的提示词只描述一个领域短而清晰。每个智能体只挂自己需要的工具减少误调用。路由和智能体可以分别测试、分别部署。后续增加新领域只需加一个智能体和一条路由规则。1.4 四个智能体的职责边界四个智能体分别是预算智能体、场地与供应商智能体、宾客智能体、流程智能体。智能体职责核心工具典型提问预算智能体拆解总预算、校验收支项、给出分配比例check_budget20 万预算怎么分配比较合理场地与供应商智能体按城市、桌数、价格筛选场地推荐供应商query_venues、query_vendors杭州适合 200 人的户外场地有哪些宾客智能体计算桌数、给出主桌和分桌建议generate_guest_layout200 个宾客需要订多少桌流程智能体把关键环节转换成时间表generate_timeline帮我排一下婚礼当天流程路由器无法匹配时走 general 分支由总策划模型直接回答综合问题。这样即使问答不在四个领域内系统也不会报错。注意智能体拆分不是越多越好。每个智能体都需要模型上下文、工具注册和测试成本。中配项目四个足够再多一个领域再拆一个。2. 环境准备Python、依赖和模型服务写代码前先把环境固定下来。多智能体依赖的库版本变化比较快项目里建议使用虚拟环境并把依赖写入 requirements.txt。2.1 建议的 Python 环境本教程示例基于 Python 3.11。3.10 也可以但低于 3.10 时部分新版本依赖可能不支持。python --version确认版本后创建虚拟环境python -m venv .venv source .venv/bin/activateWindows 环境使用.venv\Scripts\activate激活后命令行前面会出现(.venv)前缀说明当前在虚拟环境中。2.2 requirements.txt 与安装命令在项目根目录创建 requirements.txtlangchain0.3.0 langchain-core0.3.0 langchain-openai0.2.0 langgraph0.2.60 streamlit1.38.0 python-dotenv1.0.0 pydantic2.7.0安装命令pip install -r requirements.txt这里有几个版本相关的坑。第一langchain和langgraph的版本升级很快上面的版本号是常见起点落地前先确认你自己环境里的版本。第二create_react_agent的参数在不同版本里有差异第 7 节会专门讲。第三如果你不需要langchain-openai以外的模型接入不要随意安装一堆额外包避免版本冲突。2.3 模型服务选择模型调用是本项目唯一的外部依赖。为了让教程可复现建议把模型配置抽成环境变量。在项目根目录创建.env.exampleLLM_MODELqwen-plus LLM_API_KEYyour_api_key_here LLM_BASE_URLhttps://dashscope.aliyuncs.com/compatible-mode/v1如果你本机有 Ollama可以临时指向本地模型LLM_MODELqwen2.5:7b LLM_API_KEYEMPTY LLM_BASE_URLhttp://localhost:11434/v1两种方式分别适用不同阶段方案适用阶段需要条件注意点本地 Ollama学习、断网调试、成本为零安装 Ollama 并拉取模型小模型工具调用稳定性一般结构化输出支持有限云端兼容接口中配演示、开发联调申请对应平台密钥并配置 base_url需要网络、密钥和账号额度这里的关键点是本项目的路由依赖with_structured_output也就是模型需要支持函数调用或者结构化输出。如果本地小模型不支持路由部分要退化为 JSON 解析方式第 7 节会给出替代方案。2.4 项目目录结构推荐按下面的结构组织代码。planner包放核心逻辑data放模拟数据根目录的app.py只做 Streamlit 展示。wedding_planner/ ├── app.py ├── requirements.txt ├── .env.example ├── data/ │ ├── venues.json │ └── vendors.json └── planner/ ├── __init__.py ├── config.py ├── tools.py ├── agents.py └── orchestrator.py__init__.py可以是空文件作用是让planner成为可导入的包。config.py负责读取环境变量并创建模型实例。config.py 内容如下import os from dotenv import load_dotenv from langchain_openai import ChatOpenAI load_dotenv() def get_llm(temperature: float 0.2) - ChatOpenAI: return ChatOpenAI( modelos.getenv(LLM_MODEL, qwen-plus), api_keyos.getenv(LLM_API_KEY, EMPTY), base_urlos.getenv(LLM_BASE_URL, http://localhost:11434/v1), temperaturetemperature, )load_dotenv()会把.env文件中的变量加载到环境变量里。模型实例统一通过get_llm()创建后续想换模型只改.env不动业务代码。3. 先用 JSON 搭一个可复现的数据和工具底座智能体本身不产生数据它靠工具获取数据并完成计算。为了不引入数据库和网络请求本项目用本地 JSON 文件模拟业务数据。3.1 venues.json 字段设计创建data/venues.json[ { name: 湖畔草坪庄园, city: 杭州, style: 户外草坪, max_guests: 220, price_per_table: 4888, minimum_tables: 8, contact: 0571-88886666 }, { name: 城市之光宴会厅, city: 杭州, style: 现代宴会厅, max_guests: 300, price_per_table: 6888, minimum_tables: 10, contact: 0571-66668888 }, { name: 江南别院, city: 杭州, style: 中式庭院, max_guests: 160, price_per_table: 5800, minimum_tables: 6, contact: 0571-55552222 } ]字段含义字段含义用途name场地名称展示给用户city所在城市筛选条件style场地风格推荐参考max_guests最大接待人数判断是否容纳宾客price_per_table每桌价格预算匹配minimum_tables最低桌数预算匹配contact联系电话模拟真实信息3.2 vendors.json 供应商数据创建data/vendors.json[ { name: 拾光摄影工作室, city: 杭州, category: 摄影摄像, price: 9800, rating: 4.8 }, { name: 甜梦宴会设计, city: 杭州, category: 婚庆设计, price: 32000, rating: 4.7 }, { name: 金话筒主持人团队, city: 杭州, category: 主持, price: 6800, rating: 4.9 }, { name: 花间集化妆造型, city: 杭州, category: 化妆, price: 3800, rating: 4.6 } ]真实项目中这些 JSON 会被替换成数据库表、供应商 API 或者向量检索结果。教程阶段用 JSON 的好处是零依赖、可复现、能理解工具层逻辑。3.3 tools.py 工具实现创建planner/tools.py把读取 JSON 的逻辑封装成 LangChain 工具。import json import math from pathlib import Path from langchain_core.tools import tool DATA_DIR Path(__file__).resolve().parent.parent / data def _load_json(filename: str): with open(DATA_DIR / filename, r, encodingutf-8) as f: return json.load(f) tool def query_venues(city: str, max_price_per_table: float) - str: 按城市和单桌价格上限查询婚礼场地。 Args: city: 城市名称例如 杭州、上海 max_price_per_table: 单桌价格上限单位元 venues _load_json(venues.json) result [ v for v in venues if v[city] city and v[price_per_table] max_price_per_table ] return json.dumps(result, ensure_asciiFalse, indent2) tool def query_vendors(city: str, category: str ) - str: 查询婚庆服务供应商可以按类别过滤。 Args: city: 城市名称 category: 供应商类别可选值摄影摄像、婚庆设计、主持、化妆 vendors _load_json(vendors.json) result [ v for v in vendors if v[city] city and (not category or v[category] category) ] return json.dumps(result, ensure_asciiFalse, indent2) tool def check_budget(total_budget: float, expense_items: str) - str: 校验婚礼预算分配是否超支。 Args: total_budget: 婚礼总预算单位元 expense_items: JSON 数组字符串例如 [{name: 场地, amount: 80000}] try: items json.loads(expense_items) except json.JSONDecodeError: return json.dumps({error: expense_items 不是合法 JSON}, ensure_asciiFalse) total sum(item.get(amount, 0) for item in items) over_amount total - total_budget return json.dumps({ total_budget: total_budget, planned_total: total, over_budget: over_amount 0, over_amount: over_amount, items: items, }, ensure_asciiFalse, indent2) tool def generate_guest_layout(guest_total: int, table_capacity: int 10) - str: 根据宾客总数和每桌人数计算需要预订的桌数。 Args: guest_total: 宾客总人数 table_capacity: 每桌人数默认 10 table_count math.ceil(guest_total / table_capacity) main_table 1 elder_tables max(2, math.ceil(guest_total * 0.2 / table_capacity)) friend_tables max(0, table_count - main_table - elder_tables) return json.dumps({ guest_total: guest_total, table_capacity: table_capacity, table_count: table_count, suggestion: ( f建议预订 {table_count} 桌 f其中主桌 {main_table} 桌、长辈桌 {elder_tables} 桌、朋友桌 {friend_tables} 桌 ) }, ensure_asciiFalse) tool def generate_timeline(key_nodes: str) - str: 生成婚礼当天流程时间表。 Args: key_nodes: 逗号分隔的关键流程节点例如 迎亲,外景,仪式,午宴,下午茶,晚宴 nodes [n.strip() for n in key_nodes.split(,) if n.strip()] timeline [] hour 8 for node in nodes: timeline.append({time: f{hour:02d}:00, node: node}) hour 2 return json.dumps(timeline, ensure_asciiFalse, indent2)代码里有几个关键点。tool装饰器会把函数转换成 LangChain 的 Tool 对象函数签名和类型注解会被解析成模型可读的工具 Schema。所以参数的注释必须写清楚模型靠这些注释决定传什么值。所有工具都返回 JSON 字符串而不是 Python 对象。这是为了让工具输出在 ReAct 循环里可以被当作普通文本继续交给模型处理避免出现不可序列化的结构。check_budget里对expense_items做了json.JSONDecodeError捕获。模型在生成复杂参数时偶尔会生成非法 JSON捕获后返回友好错误比直接抛异常更容易让模型自愈。3.4 工具为什么写成函数而不是直接放提示词有人会问数据量这么小直接把 JSON 贴进系统提示词不行吗短期可以但会带来两个问题。第一提示词里塞大量数据会占用上下文窗口模型在长文本中检索信息的准确率会下降。第二数据一变提示词就要重新调整工具调用链断裂。把数据访问收敛到工具层之后智能体只关心“调用哪个工具”不关心数据在哪、长什么样。这也是多智能体与普通 Prompt 工程的分界线。4. 创建四个专业智能体和路由编排工具层完成后接下来创建智能体。这一步要做两件事定义四个专业智能体定义意图路由器。4.1 agents.py每个智能体就是 ReAct Agent创建planner/agents.pyfrom langgraph.prebuilt import create_react_agent from .config import get_llm from .tools import ( query_venues, query_vendors, check_budget, generate_guest_layout, generate_timeline, ) BUDGET_PROMPT 你是婚礼预算智能体负责拆解婚礼总预算并校验是否超支。 调用 check_budget 工具前先把用户描述的预算项目整理成 JSON 数组。 只输出最终预算建议和风险提示不要编造工具没有返回的数据。 VENUE_PROMPT 你是场地与供应商智能体负责推荐婚礼场地和婚庆服务商。 先调用 query_venues 查场地再根据用户需要调用 query_vendors 查供应商。 推荐时必须给出筛选依据并明确标注价格、容量和风格。 GUEST_PROMPT 你是宾客安排智能体负责计算桌数并给出座位分组建议。 调用 generate_guest_layout 得到桌数结果再补充主桌、长辈桌、朋友桌的安排说明。 FLOW_PROMPT 你是流程编排智能体负责把用户提到的婚礼关键节点转换成时间表。 先调用 generate_timeline 生成基础时间表再补充每个环节的建议事项。 def _build_agent(system_prompt: str, tools: list): # 较新版本 LangGraph 使用 prompt 参数如果安装版本较老 # 把 promptsystem_prompt 改为 state_modifiersystem_prompt return create_react_agent( get_llm(), toolstools, promptsystem_prompt, ) def create_budget_agent(): return _build_agent(BUDGET_PROMPT, [check_budget]) def create_venue_agent(): return _build_agent(VENUE_PROMPT, [query_venues, query_vendors]) def create_guest_agent(): return _build_agent(GUEST_PROMPT, [generate_guest_layout]) def create_flow_agent(): return _build_agent(FLOW_PROMPT, [generate_timeline])每个智能体由三部分组成模型实例、工具列表、系统提示词。系统提示词的作用是限定行为边界比如预算智能体必须先把预算项整理成 JSON 才能调用工具。这里不需要写太长的规则重点是让模型知道“什么时候该用哪个工具”。create_react_agent会构建一个完整的 ReAct 循环模型根据用户问题决定是否调用工具、读取工具结果、继续推理直到给出最终回答。智能体调用后的返回值是一个字典里面有完整的消息列表。4.2 orchestrator.py用结构化输出做意图路由创建planner/orchestrator.pyfrom langchain_core.prompts import ChatPromptTemplate from pydantic import BaseModel, Field from .config import get_llm from .agents import ( create_budget_agent, create_venue_agent, create_guest_agent, create_flow_agent, ) class RouteDecision(BaseModel): agent: str Field( description可选值budget、venue、guest、flow、general ) reason: str Field(description选择该智能体的一句话理由) ROUTER_PROMPT 你是婚礼策划系统的意图路由器。只负责判断用户问题应由哪个智能体处理不要回答婚礼策划内容。 判断规则 - 问题包含预算、分配、超支、费用相关词选择 budget - 问题包含城市、场地、酒店、宴会厅、供应商、摄影、主持相关词选择 venue - 问题包含人数、桌数、宾客、座位、亲友相关词选择 guest - 问题包含流程、时间表、当天安排、顺序相关词选择 flow - 其他综合问题选择 general AGENT_NAME_MAP { budget: 预算智能体, venue: 场地与供应商智能体, guest: 宾客智能体, flow: 流程智能体, general: 总策划智能体, } class WeddingPlanner: def __init__(self): self.llm get_llm(temperature0) self.router self._build_router() self.agents { budget: create_budget_agent(), venue: create_venue_agent(), guest: create_guest_agent(), flow: create_flow_agent(), } def _build_router(self): prompt ChatPromptTemplate.from_messages([ (system, ROUTER_PROMPT), (human, 用户问题{query}), ]) return prompt | self.llm.with_structured_output(RouteDecision) def route(self, query: str) - RouteDecision: return self.router.invoke({query: query}) def run_agent(self, agent_name: str, query: str) - str: agent self.agents[agent_name] result agent.invoke({ messages: [{role: user, content: query}] }) return result[messages][-1].content def run(self, query: str) - dict: decision self.route(query) if decision.agent general: answer self.llm.invoke(query).content else: answer self.run_agent(decision.agent, query) return { agent: decision.agent, agent_name: AGENT_NAME_MAP.get(decision.agent, 总策划智能体), answer: answer, }这里的核心是with_structured_output(RouteDecision)。它要求模型返回一个符合RouteDecision结构的对象而不是自由文本。使用结构化输出的好处是路由结果稳定代码里直接读取decision.agent和decision.reason不需要再做字符串解析。temperature0用于路由模型减少随机性。回答问题的智能体模型可以保留默认温度让回答语气更自然。4.3 路由判错时的兜底回答路由一定会出错。用户可能问“预算 20 万能不能办草坪婚礼”这里面既有预算又有场地路由器选哪个都算对关键在于系统不会崩。run()方法里当路由结果是general时由主模型直接回答。这个兜底逻辑保证了所有输入都能得到响应。实际项目中还可以记录路由决策日志把decision.reason存下来用于后续评估路由准确率。4.4 多轮对话在中配版本里做到什么程度中配版本里路由和智能体都不维护历史会话。每次run()调用都是独立处理用户问第二次时系统不会记得之前的预算数字。这是刻意做的简化。多轮记忆需要引入 LangGraph 的 Checkpointer比如SqliteSaver或MemorySaver把消息历史持久化。第 8 节会给出升级方向。对于本教程先保证单轮问答链路完整再谈记忆。5. 用 Streamlit 把多智能体包装成聊天界面核心逻辑完成后用 Streamlit 搭建前端。Streamlit 的聊天组件非常适合快速做智能体 Demo代码量小交互也够用。5.1 app.py 完整实现创建app.pyimport streamlit as st from planner.orchestrator import WeddingPlanner st.set_page_config(page_title多智能体婚礼策划助手, layoutwide) st.title(多智能体婚礼策划助手) st.cache_resource def get_planner() - WeddingPlanner: return WeddingPlanner() planner get_planner() if messages not in st.session_state: st.session_state.messages [] for msg in st.session_state.messages: with st.chat_message(msg[role]): st.markdown(msg[content]) prompt st.chat_input(描述你的婚礼需求例如杭州 20 万预算 200 位宾客) if prompt: st.session_state.messages.append({role: user, content: prompt}) with st.chat_message(user): st.markdown(prompt) with st.chat_message(assistant): with st.spinner(正在协调多个智能体...): result planner.run(prompt) st.caption(f当前由 {result[agent_name]} 处理) st.markdown(result[answer]) st.session_state.messages.append({ role: assistant, content: result[answer], })代码逻辑很直接st.chat_input接收用户输入。st.chat_message渲染气泡式的用户消息和助手消息。st.spinner在模型调用期间显示等待状态。st.session_state.messages保存对话记录每次点击都会重新渲染整个页面但状态保留在会话中。注意st.session_state.messages只保存角色和文本内容不保存智能体运行状态。这是合理的因为每次回答都被转成了普通文本。5.2 st.cache_resource 的作用和边界st.cache_resource让WeddingPlanner实例只初始化一次。如果没有这个装饰器Streamlit 每次交互都会重新创建模型连接和智能体对象页面会明显卡顿。这里要注意边界缓存的实例内部保存了四个create_react_agent对象如果模型配置或工具代码发生变化需要刷新页面或者重启streamlit run才能生效。开发调试阶段如果改了agents.py、tools.py里的代码建议直接重启 Streamlit 进程避免缓存干扰。5.3 显示当前由哪个智能体处理st.caption(f当前由 {result[agent_name]} 处理)这一行的教学价值比界面价值更大。它把路由决策可视化你马上能看到“用户问预算系统确实走了预算智能体”。联调时这条信息能快速暴露路由问题。6. 启动、验证和调试链路界面写完后按下面的顺序启动和验证。6.1 启动命令在项目根目录执行streamlit run app.py启动成功后终端会显示 Local URL默认是http://localhost:8501。用浏览器打开就能看到聊天界面。6.2 四组验证输入和预期输出不要只验证“能聊天”要按智能体逐个验证。用户输入期望路由验证点我只有 20 万预算怎么分配合理budget返回预算分配比例和超支风险杭州适合 200 人的户外草坪场地有哪些venue返回场地名称、价格、容量200 个宾客需要订多少桌guest返回桌数和分桌建议帮我排一下婚礼当天流程包含迎亲、仪式、午宴flow返回带时间的时间表今天天气怎么样general兜底回答不报错每一组输入都看两个地方页面上的回答内容以及st.caption显示的智能体名称。如果智能体名称和预期不一致优先检查路由规则。6.3 看不到中间过程打开 LangChain 调试模式多智能体调试时最常遇到的问题是“模型到底有没有调用工具”。在orchestrator.py顶部临时加一行from langchain.globals import set_debug set_debug(True)打开后终端会输出每一步的 Prompt、工具调用参数、工具返回结果。这是定位智能体行为最直接的方式。调试完成后记得删掉否则生产环境日志会非常嘈杂。6.4 单独测试一个智能体绕过 Streamlit如果怀疑某个智能体有问题不要在页面上反复点直接用 Python 命令测试。python -c from planner.agents import create_venue_agent; a create_venue_agent(); r a.invoke({messages: [{role: user, content: 杭州适合200人以内的草坪场地有哪些}]}); print(r[messages][-1].content)这条命令把场地智能体当成独立程序运行输出会直接打印最终回答。如果这一步就出错问题一定在智能体内部而不是 Streamlit 层。7. 常见问题排查从现象到根因多智能体项目的问题链路比普通 Web 应用长排查时要一层层剥开。先把常见现象整理成表。问题现象常见原因检查方式处理建议模型调用报 401 / 连接失败API_KEY、BASE_URL 配置错误或者.env未加载检查.env文件打印os.getenv(LLM_BASE_URL)确认字段名与代码一致确认服务商支持 OpenAI 兼容协议路由总是落到 general模型太弱或者不支持结构化输出打开调试模式看路由 Prompt 和模型返回换更强的模型改用 JsonOutputParser 解析 JSONcreate_react_agent 报参数不存在LangGraph 版本太旧不认识prompt参数执行pip show langgraph查看版本旧版把prompt换成state_modifier升级依赖工具反复调用、不输出结果工具返回内容模糊或模型陷入循环打开调试模式看工具调用序列简化工具返回显式设置recursion_limit50Streamlit 页面白屏或点击无反应依赖冲突或者缓存对象异常查看streamlit run终端日志重启进程升级或降级 streamlit回答里中文乱码JSON 输出时没有指定 UTF-8检查工具里json.dumps(..., ensure_asciiFalse)统一使用 UTF-8 编码读取文件路由选对但回答不相关智能体系统提示词不够明确查看智能体实际输出和工具调用补充工具使用规则减少“自由发挥”空间7.1 路由结构化输出不兼容时的替代方案如果你的模型服务不支持with_structured_output路由部分可以改成 JSON 输出解析。from langchain_core.output_parsers import JsonOutputParser router_chain prompt | self.llm | JsonOutputParser() def route(self, query: str) - dict: result router_chain.invoke({query: query}) return result这种方式要求模型输出的文本本身是合法 JSON。为了提升成功率在 ROUTER_PROMPT 里加上一句“只输出 JSON格式为 {agent: ..., reason: ...}”。它比结构化输出弱但兼容性更广。7.2 工具循环和 recursion_limitReAct 智能体碰到复杂问题时模型可能反复调用同一个工具或者连续多次不产出最终回答。LangGraph 默认有递归步数限制超出后抛异常。显式调高限制result agent.invoke( {messages: [{role: user, content: query}]}, config{recursion_limit: 50} )但不要依赖加高限制解决问题。根本原因通常是工具返回信息不足或者模型不理解工具结果。先把工具返回内容写清楚再考虑限制问题。7.3 数据文件路径错误工具使用Path(__file__).resolve().parent.parent / data定位数据目录。这个写法与当前工作目录无关无论从哪里启动 Streamlit 都能找到文件。如果你在调试时手动改了路径注意data目录一定要放在项目根目录和planner包平级。8. 从中配往生产走的工程化建议本教程的项目能跑通但离生产还有一段距离。下面按优先级列出升级方向。8.1 数据层替换JSON 模拟数据换成真实数据源时工具函数内部逻辑不变只改实现场地数据改成 MySQL 或 PostgreSQL 查询。供应商数据改成调用真实供应商 API。宾客数据改成从 Excel 或 CRM 系统导入。非结构化资料比如婚庆案例文档接入向量数据库做检索增强。关键是保持工具函数的入参和返回结构不变这样智能体代码不需要大改。8.2 加入记忆和人工确认多轮对话需要给智能体加 Checkpointer。以 LangGraph 为例可以用MemorySaver做进程内记忆用SqliteSaver做持久化记忆。生产级方案还会在关键节点插入人工确认。比如用户选定场地后系统先生成方案再进入人工确认节点确认后才执行下一步。这在多数业务系统里是刚需不能赌模型完全可靠。8.3 可观测性和评估多智能体系统的调试成本远高于单链系统必须从第一天就记录日志。建议至少记录每个请求的路由决策agent和reason。每个智能体调用了几次工具、调用了哪些工具。模型响应耗时的 token 数。最终回答长度和关键结果字段。评估方面准备一批覆盖四个领域的测试问题定期跑一遍统计路由准确率和回答完成率。路由准确率不达标时优先优化 ROUTER_PROMPT而不是盲目换模型。8.4 从代码级扩展方向如果想把系统从“中配”升级成“高配”可以从三个方向切入。第一改用 LangGraph 的 Supervisor 模式。让总策划智能体动态决定调用哪个子智能体而不是靠路由器的固定分类。第二给工具加权限和审计。不同角色能调用不同工具每次调用都留痕这在企业内部系统里尤其重要。第三让流程智能体真正“编排”其他智能体。流程智能体在生成时间表时可以调用场地智能体和宾客智能体的结果形成一次真实的协同处理。8.5 发布前检查清单上线前过一遍下面的清单[ ].env已加入.gitignore密钥没有提交到代码仓库。[ ] 模型服务支持函数调用或结构化输出并在内网或目标网络环境实测通过。[ ] 四个智能体分别用标准测试问题验证过路由结果符合预期。[ ] 打开调试协议验证过至少一次完整工具调用链。[ ] 工具函数对非法输入有兜底如check_budget的 JSON 解析异常。[ ] 数据文件路径重新确认不依赖当前工作目录。[ ] 检查过依赖版本langgraph的create_react_agent参数与代码一致。[ ] 日志记录了路由决策、工具调用、耗时和失败信息。多智能体项目真正难的部分从来不是写一个 Agent而是让多个 Agent 在清晰的边界里协作并且让每一条协作路径都可验证、可回滚、可观察。先从四个智能体和一条路由跑通再逐步增加记忆、人工确认和真实数据源这条路比一开始就追求复杂状态图稳妥得多。