SmartBizTrip 纵向流程图:主助手核心方法与清晰分支
中心纵向主流程,分支在左右泳道内处理后汇入。每个核心方法旁配有数据或示例,包含追问、规划、城市服务、A2A票务、MCP和SSE。
SmartBizTrip:主助手 ChatService 怎么处理一次请求
沿中间主线往下看;分支只在左右泳道展开,再汇入共同步骤。示例:查 2026-05-01 北京到成都的二等座高铁票。
先认角色:职责身份和通信身份是两回事
ChatService|主助手 / 总协调员
接住问题、识别意图、选择处理路径
调用子助手时,它扮演 A2A 客户端
生活例子:差旅服务台总台
TicketAssistant|子助手 / 票务专员
接收 A2A 任务,负责票务问题
在 A2A 调用中,它是服务端
生活例子:接到工单的票务窗口
query_train|工具 / 查票功能
执行具体车次查询,不负责理解全局需求
生活例子:票务专员使用的查询系统
通过 MCP 暴露给票务助手
网页 / FastAPI|接待入口
接收问题、把回复传回网页
不负责判断该找哪个子助手
生活例子:前台接待员
先看两个协作概念:它们连接的对象不同
A2A|Agent 与 Agent 之间交接任务
解决:主助手怎样把一个任务交给子助手,并收回任务结果
本项目:ChatService(A2A 客户端) → Task → TicketServer(服务端)
Task 携带任务;status / artifacts 表示处理状态和结果
生活例子:总协调员派工单给票务专员,之后收回执
MCP|Agent 与工具 / 数据能力之间的连接方式
解决:助手怎样发现并调用具体工具,而不是再找一个助手
本项目:TicketAssistant → query_train → TicketService / MySQL
@ticket_mcp.tool 注册工具;工具输入参数并返回查询结果
生活例子:票务专员使用订票系统窗口查车次
记忆:A2A 是“助手交办给助手”;MCP 是“助手调用工具”。后续流程图中会看到这两段实际调用。
系统启动(先准备好服务,用户发消息时才开始下面的请求流程)
API 服务创建主助手
api_server.py → ChatService.__init__()
准备 LLM、memory、AgentNetwork / A2AClient
子助手服务独立启动
TicketServer.run_server(..., 5006)
等待 A2A 任务;并非由 ChatService 启动
MCP 工具服务注册查票工具
mcp_ticket_server.py → @ticket_mcp.tool
query_train 可供票务助手按需调用
用户问题从这里进入;下面的 SSE 分段只发生在回复方向
用户提出需求
“查 2026 年 5 月 1 日北京到成都的二等座高铁票。”
网页固定把完整问题 POST 到聊天接口
static/index.html → fetch('/api/chat/stream')
请求示例:{"message":"查 2026-05-01 北京到成都的二等座高铁票"}
FastAPI 路由接收请求
@app.post('/api/chat/stream') → StreamingResponse(sse_generator(...))
它把 request.message 交给生成器;此时还没有调用子助手
SSE 生成器调用业务方法
async for chunk in chat_service.chat_stream(message)
输入仍是完整用户问题;生成器负责稍后转发回复片段
进入主助手:ChatService.chat_stream(user_input)
总协调员开始处理本轮用户需求
一条用户需求 → 一次主流程调度
1|先保存用户消息
self.memory.add_message("user", user_input)
只是记住本轮内容,还没有查车票
本步记录
“查 2026-05-01 北京到成都二等座高铁票”
后续识别可结合近期对话、偏好和任务上下文
生活例子:总台先把客人的原话记进服务记录本
2|intent_agent(user_input):理解需求并分类
intent_prompt() | self.llm → chain.invoke(...)
模型读取:本轮问题、短期对话、日期、用户偏好、任务上下文
JSON 经清理 / 解析后交回 chat_stream
生活例子:把“我想查票”整理成给票务专员的工单
本步返回(示意)
intents = ['train']
user_queries['train'] = '查 2026-05-01 北京到成都二等座'
follow_up_message = ''
输出的是“要做什么 / 怎么问专员 / 是否要追问”
实际字段内容由模型按提示词生成
follow_up_message 是否非空?
或 intents 是否包含 out_of_scope
是:追问 / 范围外
否:信息够了
本轮先回复用户,然后结束本轮处理
yield follow_up_message
缺日期示例:“请问您计划哪天出发?”
跳过规划和子助手;用户补充后会开始新一轮请求
本例 follow_up_message = '',继续往下
信息已包括日期、出发地、目的地和席别
所以下一步才判断需不需要规划
左右两条线各走各的,不在此处交叉回流
_should_skip_planning(intents)
规则判断:要不要调用规划模型?
是:跳过规划
否:调用规划模型
简单路径|直接执行意图
单一意图通常跳过;有 complex 通常进入规划
本例 intents=['train'] → 直接查票,不额外规划
多个独立查询也可能跳过,按项目规则判断
下一步:交给 _call_agent_intent_stream()
规划路径|planning_agent(intents, user_queries)
只有规则没有跳过时,才请求 LLM 评估计划
返回 need_plan 和 steps
例如多个有关联的差旅事项可能需要排先后
调用规划模型不代表一定执行计划;继续看 need_plan
need_plan?
规划模型给出的结果
否:直接执行
是:执行计划
need_plan=False
像简单路径一样直接分派
不经过 ReAct 计划执行
该分支在汇合线处进入下步
_react_loop_stream() → _execute_plan()
检查依赖,按顺序执行 execute_step()
示例:先确认行程时间,再安排接送
多个步骤结果收集后统一汇总
汇合:简单路径 / 计划步骤都进入对应业务处理
6|_call_agent_intent_stream(intent, query_str)
按意图进入具体业务;复杂计划由 execute_step 分步触发
intent='train'; query_str=整理后的查票问题
总台确认“这是查票需求”,然后决定联系票务专员
本例传入值
intent = 'train'
query_str = '查 2026-05-01 北京到成都二等座'
城市建议另走 city_service 特殊路径
intent == city_service?
城市建议特例,还是普通业务意图?
是:城市服务
否:普通意图
城市服务分支|不进入票务 A2A / MCP
_fetch_weather_for_city_service(query_str)
1. 先向天气子助手拿天气文本 weather_info
2. 主模型 chain.astream({query, weather_info})
例:“成都出差要注意什么?”→ 查天气 → 生成建议
这条分支产出的数据
weather_info = “天气子助手返回的天气描述”
advice_chunks = 主模型逐段生成的建议
建议 chunk 直接进入主助手的回复收集
生活例子:总台问天气窗口,再自己整理出差提醒
本分支的示例回复
“成都当天的天气是……建议携带雨具……”
示例文本用于说明结构;实际内容来自天气结果和模型
此分支完成后直接接到下方共同的回复处理
普通业务分支|A2A 子助手 → MCP 工具
conf.intent.get('train') → TicketAssistant
组装 Message / Task,发送 A2A 请求
await agent.send_task_async(task)
主助手是 A2A 客户端;子助手是服务端
TicketServer.handle_task(task)
收任务文本 → AgentExecutor.invoke()
票务专员打开工单,判断下一步做什么
AgentExecutor 选择工具或追问
to_langchain_tool('http://127.0.0.1:8001')
查票信息齐全 → 选择 query_train
参数示例:2026-05-01 / 北京 / 成都 / 二等座
MCP 工具执行查询
@ticket_mcp.tool('query_train') → TicketService
_execute_query() → train_tickets (MySQL)
示意结果:匹配车次记录;实际看数据库内容
子助手返回 A2A Task
completed + artifacts → parts[0].text
否则 → status.message(追问 / 错误等)
示例:车次文本在 artifacts;缺信息提示在 status
随后由主助手总结成面向用户的回复
7|整理用户可读答案
summarize_ticket_prompt() | self.llm → chain.astream(...)
票务原始结果先整体返回,再由主模型流式总结
计划分支先汇总各步骤;城市建议在自身分支生成
给用户的回复 chunk 示例
chunk 1: “查到了,”
chunk 2: “北京到成都有两趟……”
chunk 的边界由模型生成过程决定
不是数据库记录一行一行直接推给网页
8|chat_stream 收集并保存完整回复
parts.append(chunk) → yield chunk → ''.join(parts)
memory.add_message('assistant', response)
记忆保存完整回答,不把每个 chunk 当独立消息
这一阶段的数据变化
多段 chunk → 一条完整 assistant response
追问分支最终也会把回复交给网页并留存
用户补充信息后,再开始新一轮 chat_stream
9|SSE 生成器包装每段回复
data: {"chunk":"..."},结束时 data: [DONE]
SSE 只负责把 chunk 传到浏览器
请求上行是完整问题;回复下行才是多个 chunk
实际事件示例
data: {"chunk":"查到了,"}
data: {"chunk":"北京到成都有两趟"}
data: [DONE] 是结束信号,不是正文
网页逐段接收 SSE,把 chunk 追加到同一个聊天气泡
最终显示:“查到了,北京到成都有两趟符合条件的列车。”
这就是一次问题从网页进入、由主助手调度、再返回网页的完整闭环
看分支时记住这三个判断
① 追问分支:信息不足 → 先问用户,本轮结束;信息够 → 继续。
② 规划分支:简单意图直接执行;需要规划时再看 need_plan,计划执行后与直接路径汇合。
③ 业务分支:city_service 取天气并生成建议;普通意图找子助手,票务助手再按需调用 MCP。
连代码时追踪数据:message → user_input → intents/user_queries → Task → artifacts/status.message → chunk → SSE。