|
@@ -0,0 +1,321 @@
|
|
|
|
|
+<svg xmlns="http://www.w3.org/2000/svg" width="1800" height="5780" viewBox="0 0 1800 5780" role="img" aria-labelledby="title desc">
|
|
|
|
|
+<title id="title">SmartBizTrip 纵向流程图:主助手核心方法与清晰分支</title>
|
|
|
|
|
+<desc id="desc">中心纵向主流程,分支在左右泳道内处理后汇入。每个核心方法旁配有数据或示例,包含追问、规划、城市服务、A2A票务、MCP和SSE。</desc>
|
|
|
|
|
+<defs>
|
|
|
|
|
+ <marker id="a" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto"><path d="M0 0L10 5.5L0 11Z" fill="#52687d"/></marker>
|
|
|
|
|
+ <marker id="g" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto"><path d="M0 0L10 5.5L0 11Z" fill="#25804f"/></marker>
|
|
|
|
|
+ <style>
|
|
|
|
|
+ text{font-family:"Microsoft YaHei","Noto Sans CJK SC",Arial,sans-serif;fill:#24384b}
|
|
|
|
|
+ .title{font-size:29px;font-weight:700}.sub{font-size:15px;fill:#52687d}.section{font-size:19px;font-weight:700;fill:#315d83}
|
|
|
|
|
+ .h{font-size:16px;font-weight:700}.b{font-size:14px}.sm{font-size:12px;fill:#52687d}.c{font-family:Consolas,"Microsoft YaHei",monospace;font-size:12px;fill:#315d83}
|
|
|
|
|
+ .ex{font-size:13px;fill:#4d3c78}.lbl{font-size:13px;font-weight:700;fill:#6a4b18}
|
|
|
|
|
+ .flow{fill:none;stroke:#718397;stroke-width:2.2;marker-end:url(#a)}.green{fill:none;stroke:#25804f;stroke-width:2.3;marker-end:url(#g)}
|
|
|
|
|
+ </style>
|
|
|
|
|
+</defs>
|
|
|
|
|
+<rect width="1800" height="5780" fill="#f7f9fc"/>
|
|
|
|
|
+<text x="900" y="48" text-anchor="middle" class="title">SmartBizTrip:主助手 ChatService 怎么处理一次请求</text>
|
|
|
|
|
+<text x="900" y="78" text-anchor="middle" class="sub">沿中间主线往下看;分支只在左右泳道展开,再汇入共同步骤。示例:查 2026-05-01 北京到成都的二等座高铁票。</text>
|
|
|
|
|
+
|
|
|
|
|
+<text x="55" y="119" class="section">先认角色:职责身份和通信身份是两回事</text>
|
|
|
|
|
+<rect x="55" y="138" width="410" height="142" rx="12" fill="#eee9f8" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="78" y="170" class="h">ChatService|主助手 / 总协调员</text>
|
|
|
|
|
+<text x="78" y="201" class="b">接住问题、识别意图、选择处理路径</text>
|
|
|
|
|
+<text x="78" y="231" class="ex">调用子助手时,它扮演 A2A 客户端</text>
|
|
|
|
|
+<text x="78" y="260" class="sm">生活例子:差旅服务台总台</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="490" y="138" width="410" height="142" rx="12" fill="#e9f1fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="513" y="170" class="h">TicketAssistant|子助手 / 票务专员</text>
|
|
|
|
|
+<text x="513" y="201" class="b">接收 A2A 任务,负责票务问题</text>
|
|
|
|
|
+<text x="513" y="231" class="ex">在 A2A 调用中,它是服务端</text>
|
|
|
|
|
+<text x="513" y="260" class="sm">生活例子:接到工单的票务窗口</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="925" y="138" width="410" height="142" rx="12" fill="#fff3df" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="948" y="170" class="h">query_train|工具 / 查票功能</text>
|
|
|
|
|
+<text x="948" y="201" class="b">执行具体车次查询,不负责理解全局需求</text>
|
|
|
|
|
+<text x="948" y="231" class="ex">生活例子:票务专员使用的查询系统</text>
|
|
|
|
|
+<text x="948" y="260" class="sm">通过 MCP 暴露给票务助手</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="1360" y="138" width="385" height="142" rx="12" fill="#edf2f7" stroke="#90a9c3" stroke-width="2"/>
|
|
|
|
|
+<text x="1383" y="170" class="h">网页 / FastAPI|接待入口</text>
|
|
|
|
|
+<text x="1383" y="201" class="b">接收问题、把回复传回网页</text>
|
|
|
|
|
+<text x="1383" y="231" class="ex">不负责判断该找哪个子助手</text>
|
|
|
|
|
+<text x="1383" y="260" class="sm">生活例子:前台接待员</text>
|
|
|
|
|
+
|
|
|
|
|
+<text x="55" y="318" class="section">先看两个协作概念:它们连接的对象不同</text>
|
|
|
|
|
+<rect x="55" y="337" width="820" height="178" rx="13" fill="#eee9f8" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="80" y="371" class="h">A2A|Agent 与 Agent 之间交接任务</text>
|
|
|
|
|
+<text x="80" y="402" class="b">解决:主助手怎样把一个任务交给子助手,并收回任务结果</text>
|
|
|
|
|
+<text x="80" y="432" class="c">本项目:ChatService(A2A 客户端) → Task → TicketServer(服务端)</text>
|
|
|
|
|
+<text x="80" y="462" class="sm">Task 携带任务;status / artifacts 表示处理状态和结果</text>
|
|
|
|
|
+<text x="80" y="492" class="ex">生活例子:总协调员派工单给票务专员,之后收回执</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="925" y="337" width="820" height="178" rx="13" fill="#fff3df" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="950" y="371" class="h">MCP|Agent 与工具 / 数据能力之间的连接方式</text>
|
|
|
|
|
+<text x="950" y="402" class="b">解决:助手怎样发现并调用具体工具,而不是再找一个助手</text>
|
|
|
|
|
+<text x="950" y="432" class="c">本项目:TicketAssistant → query_train → TicketService / MySQL</text>
|
|
|
|
|
+<text x="950" y="462" class="sm">@ticket_mcp.tool 注册工具;工具输入参数并返回查询结果</text>
|
|
|
|
|
+<text x="950" y="492" class="ex">生活例子:票务专员使用订票系统窗口查车次</text>
|
|
|
|
|
+<text x="900" y="535" text-anchor="middle" class="sm">记忆:A2A 是“助手交办给助手”;MCP 是“助手调用工具”。后续流程图中会看到这两段实际调用。</text>
|
|
|
|
|
+<g transform="translate(0 235)"><text x="55" y="318" class="section">系统启动(先准备好服务,用户发消息时才开始下面的请求流程)</text>
|
|
|
|
|
+<rect x="55" y="337" width="535" height="106" rx="12" fill="#e9f1fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="78" y="370" class="h">API 服务创建主助手</text>
|
|
|
|
|
+<text x="78" y="399" class="c">api_server.py → ChatService.__init__()</text>
|
|
|
|
|
+<text x="78" y="427" class="sm">准备 LLM、memory、AgentNetwork / A2AClient</text>
|
|
|
|
|
+<rect x="632" y="337" width="535" height="106" rx="12" fill="#eee9f8" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="655" y="370" class="h">子助手服务独立启动</text>
|
|
|
|
|
+<text x="655" y="399" class="c">TicketServer.run_server(..., 5006)</text>
|
|
|
|
|
+<text x="655" y="427" class="sm">等待 A2A 任务;并非由 ChatService 启动</text>
|
|
|
|
|
+<rect x="1209" y="337" width="536" height="106" rx="12" fill="#fff3df" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="1232" y="370" class="h">MCP 工具服务注册查票工具</text>
|
|
|
|
|
+<text x="1232" y="399" class="c">mcp_ticket_server.py → @ticket_mcp.tool</text>
|
|
|
|
|
+<text x="1232" y="427" class="sm">query_train 可供票务助手按需调用</text>
|
|
|
|
|
+
|
|
|
|
|
+<text x="55" y="483" class="section">用户问题从这里进入;下面的 SSE 分段只发生在回复方向</text>
|
|
|
|
|
+<rect x="330" y="503" width="1140" height="94" rx="14" fill="#e5f4e8" stroke="#61a878" stroke-width="2"/>
|
|
|
|
|
+<text x="900" y="538" text-anchor="middle" class="h">用户提出需求</text>
|
|
|
|
|
+<text x="900" y="570" text-anchor="middle" class="ex">“查 2026 年 5 月 1 日北京到成都的二等座高铁票。”</text>
|
|
|
|
|
+<path d="M900 598V620" class="flow"/>
|
|
|
|
|
+<rect x="330" y="630" width="1140" height="100" rx="12" fill="#fff" stroke="#90a9c3" stroke-width="2"/>
|
|
|
|
|
+<text x="360" y="665" class="h">网页固定把完整问题 POST 到聊天接口</text>
|
|
|
|
|
+<text x="360" y="695" class="c">static/index.html → fetch('/api/chat/stream')</text>
|
|
|
|
|
+<text x="360" y="721" class="sm">请求示例:{"message":"查 2026-05-01 北京到成都的二等座高铁票"}</text>
|
|
|
|
|
+<path d="M900 731V753" class="flow"/>
|
|
|
|
|
+<rect x="330" y="763" width="1140" height="100" rx="12" fill="#e9f1fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="360" y="798" class="h">FastAPI 路由接收请求</text>
|
|
|
|
|
+<text x="360" y="828" class="c">@app.post('/api/chat/stream') → StreamingResponse(sse_generator(...))</text>
|
|
|
|
|
+<text x="360" y="854" class="sm">它把 request.message 交给生成器;此时还没有调用子助手</text>
|
|
|
|
|
+<path d="M900 864V886" class="flow"/>
|
|
|
|
|
+<rect x="330" y="896" width="1140" height="100" rx="12" fill="#e9f1fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="360" y="931" class="h">SSE 生成器调用业务方法</text>
|
|
|
|
|
+<text x="360" y="961" class="c">async for chunk in chat_service.chat_stream(message)</text>
|
|
|
|
|
+<text x="360" y="987" class="sm">输入仍是完整用户问题;生成器负责稍后转发回复片段</text>
|
|
|
|
|
+<path d="M900 997V1019" class="flow"/>
|
|
|
|
|
+<rect x="330" y="1029" width="1140" height="106" rx="16" fill="#7155a5" stroke="#543d83" stroke-width="2.5"/>
|
|
|
|
|
+<text x="900" y="1065" text-anchor="middle" class="h" style="fill:#fff;font-size:20px">进入主助手:ChatService.chat_stream(user_input)</text>
|
|
|
|
|
+<text x="900" y="1098" text-anchor="middle" class="b" style="fill:#fff">总协调员开始处理本轮用户需求</text>
|
|
|
|
|
+<text x="900" y="1123" text-anchor="middle" class="sm" style="fill:#eee9f8">一条用户需求 → 一次主流程调度</text>
|
|
|
|
|
+<path d="M900 1136V1158" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Core step 1 -->
|
|
|
|
|
+<rect x="180" y="1168" width="1440" height="150" rx="13" fill="#f1edfa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 1168V1318" stroke="#c3b5dc" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="1203" class="h">1|先保存用户消息</text>
|
|
|
|
|
+<text x="210" y="1235" class="c">self.memory.add_message("user", user_input)</text>
|
|
|
|
|
+<text x="210" y="1265" class="b">只是记住本轮内容,还没有查车票</text>
|
|
|
|
|
+<text x="930" y="1203" class="h">本步记录</text>
|
|
|
|
|
+<text x="930" y="1235" class="ex">“查 2026-05-01 北京到成都二等座高铁票”</text>
|
|
|
|
|
+<text x="930" y="1265" class="sm">后续识别可结合近期对话、偏好和任务上下文</text>
|
|
|
|
|
+<text x="930" y="1295" class="sm">生活例子:总台先把客人的原话记进服务记录本</text>
|
|
|
|
|
+<path d="M900 1319V1340" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Core step 2 -->
|
|
|
|
|
+<rect x="180" y="1350" width="1440" height="235" rx="13" fill="#f1edfa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 1350V1585" stroke="#c3b5dc" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="1385" class="h">2|intent_agent(user_input):理解需求并分类</text>
|
|
|
|
|
+<text x="210" y="1417" class="c">intent_prompt() | self.llm → chain.invoke(...)</text>
|
|
|
|
|
+<text x="210" y="1447" class="b">模型读取:本轮问题、短期对话、日期、用户偏好、任务上下文</text>
|
|
|
|
|
+<text x="210" y="1477" class="b">JSON 经清理 / 解析后交回 chat_stream</text>
|
|
|
|
|
+<text x="210" y="1520" class="ex">生活例子:把“我想查票”整理成给票务专员的工单</text>
|
|
|
|
|
+<text x="930" y="1385" class="h">本步返回(示意)</text>
|
|
|
|
|
+<text x="930" y="1417" class="c">intents = ['train']</text>
|
|
|
|
|
+<text x="930" y="1447" class="c">user_queries['train'] = '查 2026-05-01 北京到成都二等座'</text>
|
|
|
|
|
+<text x="930" y="1477" class="c">follow_up_message = ''</text>
|
|
|
|
|
+<text x="930" y="1520" class="sm">输出的是“要做什么 / 怎么问专员 / 是否要追问”</text>
|
|
|
|
|
+<text x="930" y="1550" class="sm">实际字段内容由模型按提示词生成</text>
|
|
|
|
|
+<path d="M900 1586V1607" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Branch 1: follow-up -->
|
|
|
|
|
+<path d="M900 1615L1080 1680L900 1745L720 1680Z" fill="#fff3c4" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="900" y="1675" text-anchor="middle" class="h">follow_up_message 是否非空?</text>
|
|
|
|
|
+<text x="900" y="1701" text-anchor="middle" class="sm">或 intents 是否包含 out_of_scope</text>
|
|
|
|
|
+<path d="M720 1680H420V1760" class="flow"/>
|
|
|
|
|
+<path d="M1080 1680H1380V1760" class="flow"/>
|
|
|
|
|
+<text x="452" y="1710" class="lbl">是:追问 / 范围外</text>
|
|
|
|
|
+<text x="1400" y="1710" class="lbl">否:信息够了</text>
|
|
|
|
|
+<rect x="70" y="1770" width="700" height="155" rx="12" fill="#fff4e6" stroke="#d1a043" stroke-width="2"/>
|
|
|
|
|
+<text x="98" y="1805" class="h">本轮先回复用户,然后结束本轮处理</text>
|
|
|
|
|
+<text x="98" y="1837" class="c">yield follow_up_message</text>
|
|
|
|
|
+<text x="98" y="1867" class="ex">缺日期示例:“请问您计划哪天出发?”</text>
|
|
|
|
|
+<text x="98" y="1897" class="sm">跳过规划和子助手;用户补充后会开始新一轮请求</text>
|
|
|
|
|
+<rect x="1030" y="1770" width="700" height="155" rx="12" fill="#e9f5ee" stroke="#61a878" stroke-width="2"/>
|
|
|
|
|
+<text x="1058" y="1805" class="h">本例 follow_up_message = '',继续往下</text>
|
|
|
|
|
+<text x="1058" y="1837" class="b">信息已包括日期、出发地、目的地和席别</text>
|
|
|
|
|
+<text x="1058" y="1867" class="ex">所以下一步才判断需不需要规划</text>
|
|
|
|
|
+<text x="1058" y="1897" class="sm">左右两条线各走各的,不在此处交叉回流</text>
|
|
|
|
|
+<path d="M1380 1926V1950H900V1970" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Branch 2: planning choice -->
|
|
|
|
|
+<path d="M900 1975L1080 2040L900 2105L720 2040Z" fill="#fff3c4" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="900" y="2035" text-anchor="middle" class="h">_should_skip_planning(intents)</text>
|
|
|
|
|
+<text x="900" y="2061" text-anchor="middle" class="sm">规则判断:要不要调用规划模型?</text>
|
|
|
|
|
+<path d="M720 2040H420V2130" class="flow"/>
|
|
|
|
|
+<path d="M1080 2040H1380V2130" class="flow"/>
|
|
|
|
|
+<text x="450" y="2070" class="lbl">是:跳过规划</text>
|
|
|
|
|
+<text x="1398" y="2070" class="lbl">否:调用规划模型</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="70" y="2140" width="700" height="200" rx="12" fill="#e8f1fc" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="98" y="2176" class="h">简单路径|直接执行意图</text>
|
|
|
|
|
+<text x="98" y="2208" class="b">单一意图通常跳过;有 complex 通常进入规划</text>
|
|
|
|
|
+<text x="98" y="2238" class="ex">本例 intents=['train'] → 直接查票,不额外规划</text>
|
|
|
|
|
+<text x="98" y="2268" class="sm">多个独立查询也可能跳过,按项目规则判断</text>
|
|
|
|
|
+<text x="98" y="2310" class="sm">下一步:交给 _call_agent_intent_stream()</text>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="1030" y="2140" width="700" height="200" rx="12" fill="#eee9f8" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="1058" y="2176" class="h">规划路径|planning_agent(intents, user_queries)</text>
|
|
|
|
|
+<text x="1058" y="2208" class="b">只有规则没有跳过时,才请求 LLM 评估计划</text>
|
|
|
|
|
+<text x="1058" y="2238" class="c">返回 need_plan 和 steps</text>
|
|
|
|
|
+<text x="1058" y="2268" class="ex">例如多个有关联的差旅事项可能需要排先后</text>
|
|
|
|
|
+<text x="1058" y="2310" class="sm">调用规划模型不代表一定执行计划;继续看 need_plan</text>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Planner result branch, contained within right lane -->
|
|
|
|
|
+<path d="M1380 2341V2360" class="flow"/>
|
|
|
|
|
+<path d="M1380 2365L1535 2420L1380 2475L1225 2420Z" fill="#fff3c4" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="1380" y="2416" text-anchor="middle" class="h">need_plan?</text>
|
|
|
|
|
+<text x="1380" y="2441" text-anchor="middle" class="sm">规划模型给出的结果</text>
|
|
|
|
|
+<path d="M1225 2420H1115V2490" class="flow"/>
|
|
|
|
|
+<path d="M1535 2420H1645V2490" class="flow"/>
|
|
|
|
|
+<text x="1053" y="2463" class="lbl">否:直接执行</text>
|
|
|
|
|
+<text x="1650" y="2463" class="lbl">是:执行计划</text>
|
|
|
|
|
+<rect x="800" y="2500" width="390" height="166" rx="12" fill="#e8f1fc" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="824" y="2535" class="h">need_plan=False</text>
|
|
|
|
|
+<text x="824" y="2567" class="b">像简单路径一样直接分派</text>
|
|
|
|
|
+<text x="824" y="2597" class="ex">不经过 ReAct 计划执行</text>
|
|
|
|
|
+<text x="824" y="2637" class="sm">该分支在汇合线处进入下步</text>
|
|
|
|
|
+<rect x="1220" y="2500" width="510" height="166" rx="12" fill="#eee9f8" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="1245" y="2535" class="h">_react_loop_stream() → _execute_plan()</text>
|
|
|
|
|
+<text x="1245" y="2567" class="b">检查依赖,按顺序执行 execute_step()</text>
|
|
|
|
|
+<text x="1245" y="2597" class="ex">示例:先确认行程时间,再安排接送</text>
|
|
|
|
|
+<text x="1245" y="2637" class="sm">多个步骤结果收集后统一汇总</text>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Planning branches converge via a simple bus -->
|
|
|
|
|
+<path d="M420 2341V2720M995 2667V2720M1475 2667V2720" fill="none" stroke="#718397" stroke-width="2"/>
|
|
|
|
|
+<path d="M420 2720H1475" fill="none" stroke="#718397" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 2720V2740" class="flow"/>
|
|
|
|
|
+<rect x="270" y="2750" width="1260" height="52" rx="10" fill="#edf2f7" stroke="#c5d1dc"/>
|
|
|
|
|
+<text x="900" y="2783" text-anchor="middle" class="h">汇合:简单路径 / 计划步骤都进入对应业务处理</text>
|
|
|
|
|
+<path d="M900 2803V2824" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Dispatch -->
|
|
|
|
|
+<rect x="180" y="2834" width="1440" height="190" rx="13" fill="#f1edfa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 2834V3024" stroke="#c3b5dc" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="2870" class="h">6|_call_agent_intent_stream(intent, query_str)</text>
|
|
|
|
|
+<text x="210" y="2902" class="b">按意图进入具体业务;复杂计划由 execute_step 分步触发</text>
|
|
|
|
|
+<text x="210" y="2932" class="c">intent='train'; query_str=整理后的查票问题</text>
|
|
|
|
|
+<text x="210" y="2972" class="ex">总台确认“这是查票需求”,然后决定联系票务专员</text>
|
|
|
|
|
+<text x="930" y="2870" class="h">本例传入值</text>
|
|
|
|
|
+<text x="930" y="2902" class="c">intent = 'train'</text>
|
|
|
|
|
+<text x="930" y="2932" class="c">query_str = '查 2026-05-01 北京到成都二等座'</text>
|
|
|
|
|
+<text x="930" y="2972" class="sm">城市建议另走 city_service 特殊路径</text>
|
|
|
|
|
+<path d="M900 3025V3047" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Branch 3: service dispatch lanes -->
|
|
|
|
|
+<path d="M900 3055L1070 3117L900 3179L730 3117Z" fill="#fff3c4" stroke="#d0a04d" stroke-width="2"/>
|
|
|
|
|
+<text x="900" y="3112" text-anchor="middle" class="h">intent == city_service?</text>
|
|
|
|
|
+<text x="900" y="3138" text-anchor="middle" class="sm">城市建议特例,还是普通业务意图?</text>
|
|
|
|
|
+<path d="M730 3117H450V3210" class="flow"/>
|
|
|
|
|
+<path d="M1070 3117H1350V3210" class="flow"/>
|
|
|
|
|
+<text x="465" y="3155" class="lbl">是:城市服务</text>
|
|
|
|
|
+<text x="1365" y="3155" class="lbl">否:普通意图</text>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Two clean vertical lanes: no interwoven connectors -->
|
|
|
|
|
+<rect x="55" y="3220" width="790" height="790" rx="14" fill="#edf4fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<text x="82" y="3258" class="h">城市服务分支|不进入票务 A2A / MCP</text>
|
|
|
|
|
+<text x="82" y="3292" class="c">_fetch_weather_for_city_service(query_str)</text>
|
|
|
|
|
+<text x="82" y="3324" class="b">1. 先向天气子助手拿天气文本 weather_info</text>
|
|
|
|
|
+<text x="82" y="3356" class="c">2. 主模型 chain.astream({query, weather_info})</text>
|
|
|
|
|
+<text x="82" y="3392" class="ex">例:“成都出差要注意什么?”→ 查天气 → 生成建议</text>
|
|
|
|
|
+<text x="82" y="3432" class="h">这条分支产出的数据</text>
|
|
|
|
|
+<text x="82" y="3464" class="c">weather_info = “天气子助手返回的天气描述”</text>
|
|
|
|
|
+<text x="82" y="3496" class="c">advice_chunks = 主模型逐段生成的建议</text>
|
|
|
|
|
+<text x="82" y="3536" class="b">建议 chunk 直接进入主助手的回复收集</text>
|
|
|
|
|
+<text x="82" y="3576" class="sm">生活例子:总台问天气窗口,再自己整理出差提醒</text>
|
|
|
|
|
+<path d="M450 3595V3620" class="flow"/>
|
|
|
|
|
+<rect x="95" y="3630" width="710" height="148" rx="11" fill="#fff" stroke="#b3c7da"/>
|
|
|
|
|
+<text x="120" y="3665" class="h">本分支的示例回复</text>
|
|
|
|
|
+<text x="120" y="3697" class="ex">“成都当天的天气是……建议携带雨具……”</text>
|
|
|
|
|
+<text x="120" y="3727" class="sm">示例文本用于说明结构;实际内容来自天气结果和模型</text>
|
|
|
|
|
+<text x="120" y="3757" class="sm">此分支完成后直接接到下方共同的回复处理</text>
|
|
|
|
|
+<path d="M450 3780V3975H900V4220" class="green"/>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="955" y="3220" width="790" height="790" rx="14" fill="#f5f0fa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<text x="982" y="3258" class="h">普通业务分支|A2A 子助手 → MCP 工具</text>
|
|
|
|
|
+<text x="982" y="3292" class="c">conf.intent.get('train') → TicketAssistant</text>
|
|
|
|
|
+<text x="982" y="3324" class="b">组装 Message / Task,发送 A2A 请求</text>
|
|
|
|
|
+<text x="982" y="3356" class="c">await agent.send_task_async(task)</text>
|
|
|
|
|
+<text x="982" y="3392" class="ex">主助手是 A2A 客户端;子助手是服务端</text>
|
|
|
|
|
+<line x1="982" y1="3410" x2="1718" y2="3410" stroke="#d5c9e5"/>
|
|
|
|
|
+<text x="982" y="3447" class="h">TicketServer.handle_task(task)</text>
|
|
|
|
|
+<text x="982" y="3479" class="b">收任务文本 → AgentExecutor.invoke()</text>
|
|
|
|
|
+<text x="982" y="3511" class="ex">票务专员打开工单,判断下一步做什么</text>
|
|
|
|
|
+<line x1="982" y1="3529" x2="1718" y2="3529" stroke="#d5c9e5"/>
|
|
|
|
|
+<text x="982" y="3566" class="h">AgentExecutor 选择工具或追问</text>
|
|
|
|
|
+<text x="982" y="3598" class="c">to_langchain_tool('http://127.0.0.1:8001')</text>
|
|
|
|
|
+<text x="982" y="3630" class="b">查票信息齐全 → 选择 query_train</text>
|
|
|
|
|
+<text x="982" y="3662" class="ex">参数示例:2026-05-01 / 北京 / 成都 / 二等座</text>
|
|
|
|
|
+<line x1="982" y1="3680" x2="1718" y2="3680" stroke="#d5c9e5"/>
|
|
|
|
|
+<text x="982" y="3717" class="h">MCP 工具执行查询</text>
|
|
|
|
|
+<text x="982" y="3749" class="c">@ticket_mcp.tool('query_train') → TicketService</text>
|
|
|
|
|
+<text x="982" y="3781" class="c">_execute_query() → train_tickets (MySQL)</text>
|
|
|
|
|
+<text x="982" y="3813" class="ex">示意结果:匹配车次记录;实际看数据库内容</text>
|
|
|
|
|
+<line x1="982" y1="3831" x2="1718" y2="3831" stroke="#d5c9e5"/>
|
|
|
|
|
+<text x="982" y="3868" class="h">子助手返回 A2A Task</text>
|
|
|
|
|
+<text x="982" y="3900" class="c">completed + artifacts → parts[0].text</text>
|
|
|
|
|
+<text x="982" y="3932" class="c">否则 → status.message(追问 / 错误等)</text>
|
|
|
|
|
+<text x="982" y="3964" class="ex">示例:车次文本在 artifacts;缺信息提示在 status</text>
|
|
|
|
|
+<text x="982" y="3994" class="sm">随后由主助手总结成面向用户的回复</text>
|
|
|
|
|
+<path d="M1350 4011V4220" class="green"/>
|
|
|
|
|
+
|
|
|
|
|
+<!-- Common response handling -->
|
|
|
|
|
+<rect x="180" y="4230" width="1440" height="205" rx="13" fill="#f1edfa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 4230V4435" stroke="#c3b5dc" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="4267" class="h">7|整理用户可读答案</text>
|
|
|
|
|
+<text x="210" y="4299" class="c">summarize_ticket_prompt() | self.llm → chain.astream(...)</text>
|
|
|
|
|
+<text x="210" y="4329" class="b">票务原始结果先整体返回,再由主模型流式总结</text>
|
|
|
|
|
+<text x="210" y="4365" class="ex">计划分支先汇总各步骤;城市建议在自身分支生成</text>
|
|
|
|
|
+<text x="930" y="4267" class="h">给用户的回复 chunk 示例</text>
|
|
|
|
|
+<text x="930" y="4299" class="c">chunk 1: “查到了,”</text>
|
|
|
|
|
+<text x="930" y="4329" class="c">chunk 2: “北京到成都有两趟……”</text>
|
|
|
|
|
+<text x="930" y="4365" class="sm">chunk 的边界由模型生成过程决定</text>
|
|
|
|
|
+<text x="930" y="4395" class="sm">不是数据库记录一行一行直接推给网页</text>
|
|
|
|
|
+<path d="M900 4436V4457" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="180" y="4467" width="1440" height="150" rx="13" fill="#f1edfa" stroke="#947bbf" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 4467V4617" stroke="#c3b5dc" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="4503" class="h">8|chat_stream 收集并保存完整回复</text>
|
|
|
|
|
+<text x="210" y="4535" class="c">parts.append(chunk) → yield chunk → ''.join(parts)</text>
|
|
|
|
|
+<text x="210" y="4565" class="c">memory.add_message('assistant', response)</text>
|
|
|
|
|
+<text x="210" y="4595" class="sm">记忆保存完整回答,不把每个 chunk 当独立消息</text>
|
|
|
|
|
+<text x="930" y="4503" class="h">这一阶段的数据变化</text>
|
|
|
|
|
+<text x="930" y="4535" class="ex">多段 chunk → 一条完整 assistant response</text>
|
|
|
|
|
+<text x="930" y="4565" class="sm">追问分支最终也会把回复交给网页并留存</text>
|
|
|
|
|
+<text x="930" y="4595" class="sm">用户补充信息后,再开始新一轮 chat_stream</text>
|
|
|
|
|
+<path d="M900 4618V4639" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="180" y="4649" width="1440" height="176" rx="13" fill="#e9f1fb" stroke="#789ec7" stroke-width="2"/>
|
|
|
|
|
+<path d="M900 4649V4825" stroke="#c3d4e5" stroke-width="1.5"/>
|
|
|
|
|
+<text x="210" y="4686" class="h">9|SSE 生成器包装每段回复</text>
|
|
|
|
|
+<text x="210" y="4718" class="c">data: {"chunk":"..."},结束时 data: [DONE]</text>
|
|
|
|
|
+<text x="210" y="4748" class="b">SSE 只负责把 chunk 传到浏览器</text>
|
|
|
|
|
+<text x="210" y="4784" class="ex">请求上行是完整问题;回复下行才是多个 chunk</text>
|
|
|
|
|
+<text x="930" y="4686" class="h">实际事件示例</text>
|
|
|
|
|
+<text x="930" y="4718" class="c">data: {"chunk":"查到了,"}</text>
|
|
|
|
|
+<text x="930" y="4748" class="c">data: {"chunk":"北京到成都有两趟"}</text>
|
|
|
|
|
+<text x="930" y="4784" class="sm">data: [DONE] 是结束信号,不是正文</text>
|
|
|
|
|
+<path d="M900 4826V4847" class="flow"/>
|
|
|
|
|
+
|
|
|
|
|
+<rect x="330" y="4857" width="1140" height="116" rx="13" fill="#e5f4e8" stroke="#61a878" stroke-width="2"/>
|
|
|
|
|
+<text x="900" y="4895" text-anchor="middle" class="h">网页逐段接收 SSE,把 chunk 追加到同一个聊天气泡</text>
|
|
|
|
|
+<text x="900" y="4927" text-anchor="middle" class="ex">最终显示:“查到了,北京到成都有两趟符合条件的列车。”</text>
|
|
|
|
|
+<text x="900" y="4957" text-anchor="middle" class="sm">这就是一次问题从网页进入、由主助手调度、再返回网页的完整闭环</text>
|
|
|
|
|
+
|
|
|
|
|
+<text x="55" y="5030" class="section">看分支时记住这三个判断</text>
|
|
|
|
|
+<text x="70" y="5065" class="b">① 追问分支:信息不足 → 先问用户,本轮结束;信息够 → 继续。</text>
|
|
|
|
|
+<text x="70" y="5095" class="b">② 规划分支:简单意图直接执行;需要规划时再看 need_plan,计划执行后与直接路径汇合。</text>
|
|
|
|
|
+<text x="70" y="5125" class="b">③ 业务分支:city_service 取天气并生成建议;普通意图找子助手,票务助手再按需调用 MCP。</text>
|
|
|
|
|
+<text x="70" y="5175" class="sm">连代码时追踪数据:message → user_input → intents/user_queries → Task → artifacts/status.message → chunk → SSE。</text>
|
|
|
|
|
+</g>
|
|
|
|
|
+</svg>
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|