本文分两部分。第一部分是 MCP 的通用知识(第 1–8 节),只在少数地方用 Roadbook 举例;第二部分以 Roadbook(github.com/devgxy/roadbook)为例,讲工程落地(第 9–18 节)。两部分可以分开读:只关心协议的人读完第一部分即可;从第 9 节开始的人,可以回第 1–4 节补 Host/Client/Server 与传输的定义。
全文速览:
- MCP 是连接层,不是编排层:它规定工具怎么接、怎么调、怎么回,不规定谁来决定调用。
- 本地 MCP 与远程 MCP 的差别,主要在信任边界:密钥归谁、用户是谁、失败怎么发生。
- 这个项目把「取事实」交给 MCP,把「下结论」交给规则,模型只做参数抽取和可选精修。
目录
第一部分 · 知识
- MCP 解决什么问题
- Host、Client、Server
- tools、resources、prompts
- 传输:stdio 与 Streamable HTTP
- 生命周期与一次调用
- 工具契约
- 安全边界
- 编排:Agent 与管线
第二部分 · 工程
- 项目全貌
- 本地 MCP 接入
- 高德 MCP 实战
- 12306 MCP 实战
- 一次规划的流程
- 模型的两个位置
- 故障与边界
- 上远程 MCP 要补什么
- 代码地图
- 决策清单
第一部分 · 知识
1. MCP 解决什么问题
一句话:Model Context Protocol(MCP)把 N 个客户端 × M 个工具的适配,变成 N + M 次实现。
在没有协议之前,每个模型客户端要接每个工具,都得写一套私有集成:鉴权、参数格式、错误处理、结果解析各写一遍。客户端有 N 个、工具 M 个,工作量是乘法。
MCP 把中间抽成一层协议:客户端侧实现一次 Client,工具侧实现一次 Server,两边按同一套 JSON-RPC(基于 JSON 的远程调用格式)消息说话。工作量变成加法。
图 1:MCP 把 N×M 套适配变成 N+M 次实现
这件事和 LSP 很像:编辑器不再为每个语言写补全,语言不再为每个编辑器写插件,中间是一套 Language Server Protocol。MCP 是模型工具领域的同一种思路。
需要说清的是,MCP 标准化的核心是「接入」:
- 它解决的:工具怎么被发现、怎么被调用、结果怎么回传、连接怎么建立和关闭、远程怎么鉴权。
- 它不解决的:什么时候该调工具、调哪个、调几次、结果可不可信、失败怎么降级、多个工具怎么编排。
后面这些属于 Host 的编排策略;这也是为什么同一个 Server,既可以是 agent 手里的工具,也可以是确定性管线里的一个依赖。要理解这套协议怎么落地,先看三个角色。
2. Host、Client、Server
一句话:Host 管决策,Client 管连接,Server 管执行;模型不直接连 Server。
三个角色:
- Host(宿主应用):持有模型、用户界面和上下文。它决定「现在需不需要工具」,也负责把模型的选择翻译成协议调用。典型例子是聊天客户端、IDE、桌面助手。
- Client(协议客户端):Host 内部的连接器。一个 Client 对应一个 Server 连接,负责握手、列工具、发调用、收结果、处理通知。
- Server(MCP Server,工具服务):对外暴露能力。它可以是本地子进程,也可以是远端服务;它不知道对面是哪个模型,也不需要知道。
关系是 1 个 Host 带 N 个 Client,1 个 Client 连 1 个 Server:
图 2:Host、Client、Server 的连接关系
职责可以再拆细一点:
| 角色 | 负责 | 不负责 |
|---|---|---|
| Host | 持有模型、决定调用时机、维护上下文、做权限策略 | 协议消息的编解码 |
| Client | 握手、能力协商、请求/响应、通知、重连 | 决定调什么工具 |
| Server | 执行工具、返回内容、报告错误 | 理解模型、维护对话 |
「模型不直接连 Server」是理解 MCP 的关键。模型看到的是 Host 转述的工具描述,模型输出的调用也由 Host 转成tools/call。所以鉴权、限流、审计这些事都发生在 Host 与 Server 之间,不发生在模型与 Server 之间;本地 MCP 通常没有协议级鉴权,密钥由 Host 注入。
映射到这个项目:
| 角色 | 实现 |
|---|---|
| Host | agent.py的 qwen_agent 助手;或 Web 服务travel_app.py |
| Client | mcp_client.py的会话封装;agent 侧由 qwen_agent 管理 |
| Server | npx拉起的@amap/amap-maps-mcp-server、12306-mcp,以及自写的desktop_txt_mcp.py |
3. tools、resources、prompts
一句话:三种能力对应三种控制权——tools 偏模型,resources 偏应用,prompts 偏用户。
Server 可以暴露三类能力:
| 能力 | 是什么 | 谁决定使用 | 例子 |
|---|---|---|---|
tools | 可调用的函数,有参数 schema 和返回内容 | 模型(经 Host)或代码 | 查坐标、查车次 |
resources | 按 URI 读取的上下文数据 | 应用 | 文件内容、数据库记录 |
prompts | 可复用的提示词模板,带参数 | 用户 | 「按这个格式写周报」 |
这个划分很重要:不是所有能力都该做成「工具」。只读的上下文更适合 resources,固定模板更适合 prompts;把它们全塞进 tools,会让模型的选择空间变大、准确率变低。
工具是三者中唯一可能带副作用的类别,所以协议给了它最严格的契约:名字、描述、JSON Schema 参数、返回内容块、错误标记。资源是只读的,提示词是给用户选的,风险和工具不在一个量级。
Roadbook 只用了 tools 这一类:查坐标、查路线、查天气、搜 POI、查车次。没有 resources,也没有 prompts。
三类能力最终都要经过传输层,下一节看它是怎么走的。
4. 传输:stdio 与 Streamable HTTP
一句话:本地走 stdio,远程走 Streamable HTTP;差别不只在地址,而在信任边界和生命周期。
规范定义了两类传输。
stdio(本地):Host 启动一个子进程,双方在 stdin/stdout 上跑 JSON-RPC,一行一条消息;日志走 stderr,不污染协议通道。子进程的生命周期跟 Host 绑定,Host 退出,Server 也就结束。
Streamable HTTP(远程):Client 向一个 HTTP 端点 POST 消息,服务端可以用 SSE(Server-Sent Events,服务器推送)流回传;会话通过Mcp-Session-Id维持。规范为 HTTP 传输定义了基于 OAuth 2.1 的授权。更早的 HTTP+SSE 方案已经被它取代。
图 3:本地 stdio 与远程 Streamable HTTP
| 维度 | 本地 stdio | 远程 Streamable HTTP |
|---|---|---|
| 启动方式 | Host 拉起子进程 | Host 连接 URL |
| 密钥 | 环境变量,留在本机 | OAuth / Token,留在服务端 |
| 用户模型 | 单机单用户 | 多租户,需要隔离 |
| 失败模式 | 冷启动、进程崩溃 | 网络、鉴权、限流 |
| 升级 | 跟 Host 走 | 服务端独立升级 |
| 观测 | 本机日志 | 集中追踪与审计 |
这个项目的 MCP 全部是本地进程:它是桌面/单机应用,API Key 是用户自己的,放进子进程环境变量最直接。唯一的例外是配图:主路径直接调 Tavily 的 REST 接口,没有走 MCP。协议不是强制项——一个只用来补图、不需要被模型发现、也没有多工具复用价值的接口,直接 HTTP 更简单。
5. 生命周期与一次调用
一句话:连接建立一次,之后是「列工具 → 调用 → 收结果」;协议错误和工具错误要分开处理。
一次完整的 MCP 会话大致是这样:
图 4:一次 MCP 调用的生命周期
几个细节值得单独说:
- 版本协商:
initialize里带protocolVersion,Server 回它支持的版本;不兼容时 Client 应当断开,而不是硬连。 - 能力协商:双方在
initialize里声明自己支持什么,比如 Server 是否有 tools、Client 是否支持 sampling 或 roots。 - 分页:
tools/list可能带cursor,工具多的时候要翻页。 - 进度:
tools/call可以带progressToken,Server 用notifications/progress回报。 - 取消:Client 发
notifications/cancelled,但取消是「请求停止」,不是「保证已经停止」——已经产生的副作用不会回滚。 - 两类错误:协议层错误走 JSON-RPC 的
error;工具执行失败走正常结果里的isError: true,附带错误文本。前者是「消息没走通」,后者是「工具跑了但失败了」,处理方式不同。
tools/call是一条 JSON-RPC 消息:
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"maps_geo","arguments":{"address":"北京"}}}返回不是「一个结构化对象」,而是 content blocks(下面是一段示意):
{"jsonrpc":"2.0","id":7,"result":{"content":[{"type":"text","text":"{\"geocodes\":[{\"location\":\"116.40,39.90\"}]}"}],"isError":false}}这就是为什么调用方通常要做两件事:先把 blocks 拼成文本,再按业务格式解析。协议保证传得到,不保证字段稳定。
流程走通只是开始,可靠性由契约决定。
6. 工具契约
一句话:工具名和描述是给模型看的,参数 schema 是给双方校验的,返回内容块是给调用方解析的。
一个 tool 定义通常包含四类信息:
| 字段 | 作用 | 谁在用 |
|---|---|---|
name | 唯一标识 | 模型、Client、Server |
description | 说明「什么时候该用它」 | 模型 |
inputSchema | 参数的 JSON Schema | 模型生成参数、Server 校验 |
annotations | 可选;只读、破坏性、幂等、开放世界等提示 | Host 做策略 |
annotations是提示,不是强制约束。比如readOnlyHint: true表示「这个工具不该改数据」,Host 可以据此决定要不要弹确认,但 Server 仍然可能改——安全不能只靠注解。
返回侧,content blocks 支持文本、图片、音频、内嵌资源;较新的规范版本还引入了structuredContent,让工具既能给模型看文本,也能给程序看结构。这个项目锁的是mcp==1.7.1,主路径仍按 content blocks 处理。
工具设计里最容易踩的三个坑:
- 参数类型含糊。路径规划要的是坐标,不是城市名。如果描述没写清,模型就会拿地名去调,然后失败。这个项目因此把流程拆成「先
maps_geo换坐标,再maps_direction_driving」。 - 描述写的是「做什么」,不是「什么时候用」。模型靠描述选工具;
maps_text_search和maps_geo都能处理地名,描述不区分,模型就会选错。 - 不要默认工具是幂等的。协议不提供幂等键,重试是否安全由工具设计决定。查天气可以重试,下单不能。
契约解决的是单次调用,信任问题在调用之外。
7. 安全边界
一句话:本地 MCP 的风险是「以你的权限运行」,远程 MCP 的风险是「以别人的身份访问」。
本地 Server 以当前用户权限运行,能读文件、能发网络请求、能拿环境变量里的 Key。风险包括:
- 工具输出是不可信输入。Server 返回的文本会进入模型上下文,可能包含提示注入。Host 要限制长度、隔离来源,不能把工具输出当系统指令。
- Key 在环境变量里。只要 Server 进程或日志泄露,Key 就泄露;不要把 Key 放进模型上下文,也不要写进错误信息。
- 权限过大。一个只该读文件的 Server,不应该有网络权限;一个只该查天气的工具,不应该能删数据。
远程 Server 多了一层身份问题。当前规范为 HTTP 传输定义了基于 OAuth 2.1 的授权,强调几件事:
- 不要 token passthrough:Client 不能把用户 token 原样转给下游服务,必须做 audience 校验。
- 资源指示器:请求里声明目标资源,防止 token 被拿去访问别的服务。
- 租户隔离:一个 Server 服务多个用户时,工具调用必须带用户上下文,不能共享缓存或会话。
- SSRF 与参数注入:工具参数里出现 URL、路径、SQL 时,Server 侧必须校验,不能直接拼。
一句话:本地 MCP 的信任边界是「这台机器」,远程 MCP 的信任边界是「这个租户」。换传输,安全模型要跟着换。
边界之外,最后一个知识问题是编排。
8. 编排:Agent 与管线
一句话:MCP 决定「能调什么」,编排决定「谁来调、按什么顺序调」。
同一个 Server,可以有两种完全不同的用法。
Agent 循环:模型看上下文,选工具和参数;Host 执行;结果回填;模型继续,直到它认为可以回答。
图 5:Agent 循环:模型决定下一步调用
管线:代码写死调用顺序,模型不参与调度。
图 6:管线:代码决定调用顺序
| 维度 | Agent | 管线 |
|---|---|---|
| 谁决定下一步 | 模型 | 代码 |
| 工具选择 | 描述 + 上下文 | 写死的顺序 |
| 优点 | 开放、可组合、能处理没预演过的任务 | 可测试、可复现、延迟可控 |
| 代价 | 不确定、难审计、成本高 | 覆盖面有限、改流程要改代码 |
| 适合 | 开放式任务、工具集经常变 | 强合规、强审计、结果要可复算 |
还有中间形态:路由(先用小模型选一类工具,再交给管线)、规划-执行(模型只出计划,执行由代码做)、人在环(关键步骤等确认)。选哪种,取决于你对「不确定」的容忍度。
第一部分小结:MCP 定义的是连接与调用,不是编排;Host 决定谁选工具;工具契约决定可靠性;安全模型随传输方式变化。下面进入工程。
第二部分 · 工程
9. 项目全貌
一句话:一个 Starlette 服务,一条规则规划管线,MCP 只出现在取事实的那一段。
Roadbook 做的事很窄:把一句口语收成几天自驾路书。它的工程结构比功能看起来简单:
图 7:Roadbook 的工程结构
几个关键决定:
- 规划不用 agent 框架。调用顺序是代码写死的,模型只在两个受控节点出现。
- MCP 全部是本地进程。高德、12306 用
npx起子进程;自写的桌面工具用 Python 起。 - 配图不走 MCP。直接调 Tavily REST,原因见第 4 节。
- 结果流式输出。
POST /api/plan用 SSE 把工具进度推给前端。
还有一条支线:agent.py用 qwen_agent 把同一份 MCP 配置挂给模型,做一个「模型自己选工具」的桌面助手。它和 Web 主路径共用配置,但不共用编排。
先看配置和会话,再看两个具体的 Server。
10. 本地 MCP 接入
一句话:一份配置、一个会话池、两层锁,解决「怎么起、怎么复用、怎么不互相拖死」。
配置
配置是「服务名 → 启动命令」:
{"amap-maps":{"command":"npx","args":["-y","@amap/amap-maps-mcp-server"],"env":{"AMAP_MAPS_API_KEY":"..."}},"12306-mcp":{"command":"npx","args":["-y","12306-mcp"]}}这是_build_servers()的形状;交给 qwen_agent 时,外面再包一层{"mcpServers": ...}。这里省略了desktop-txt和 Tavily。
实际代码里,高德和 Tavily 有 Key 才注册,12306 和desktop-txt无条件注册。缺 Key 时服务不出现,进程照常启动;但缺高德 Key,规划请求本身会失败——「进程能起来」和「功能可用」是两件事。
自写 Server
自己写一个本地 Server 也不复杂。desktop_txt_mcp.py用 FastMCP(MCP 的 Python 服务端框架)把两个普通函数暴露成工具:
frommcp.server.fastmcpimportFastMCP mcp=FastMCP("桌面 TXT 文件统计器")@mcp.tool()defcount_desktop_txt_files()->int:"""统计桌面上 .txt 文件的数量"""...函数名就是工具名,docstring 就是工具描述。
会话与超时
会话管理解决三个问题:
- 冷启动:
npx起一个子进程要几秒,不能每次请求都起。会话池把子进程留下,initialize()之后复用。 - 互相拖死:每个 Server 一把创建锁,一个 Server 启动卡住,不会阻塞其他 Server。
- 失败自愈:会话在上下文边界抛异常时,从池里摘掉,下次重建。
调用侧还有一把会话内锁:同一个 Server 的call_tool串行执行;高德另外限制调用起始间隔(0.36 秒),遇到 QPS 超限退避重试。这些是实现策略,不是 MCP 协议的要求。
超时是另一组必须写清的数字:
| 位置 | 超时 | 说明 |
|---|---|---|
| Server 启动 | 30 秒 | 包含启动、建会话、initialize() |
| 12306 工具 | 默认 14 秒 | 整次调用,含冷启动和等锁 |
| 12306 部分核验 | 8 秒 | 站名解析、途经核验 |
| 规划精修 | 7 秒 | 只包住模型合成 |
启动时预热高德和 Tavily;12306 不预热。原因写在代码注释里:它一启动就请求官网,网络抖动会让进程直接退出。预热失败也不影响启动,只是第一次查班次要把冷启动时间算进去。
11. 高德 MCP 实战
一句话:先把地名换成坐标,再拿坐标查路线;返回是文本,解析要兜底。
这个项目用了高德的四个工具:
| 工具 | 参数 | 返回 |
|---|---|---|
maps_geo | address | 地理编码,含location(lng,lat) |
maps_direction_driving | origin、destination(坐标) | 路线paths[0]:distance、duration、steps[].polyline |
maps_weather | city | forecasts[].casts或lives |
maps_text_search | keywords、city | POI 列表:名称、地址、照片 |
调用参数长这样:
[{"name":"maps_geo","arguments":{"address":"北京"}},{"name":"maps_direction_driving","arguments":{"origin":"116.40,39.90","destination":"104.06,30.67"}},{"name":"maps_weather","arguments":{"city":"成都"}},{"name":"maps_text_search","arguments":{"keywords":"成都 景点","city":"成都"}}]流程上有两个约束:
- 地理编码要先做。路径规划只认坐标;拿地名直接调,会得到失败或空结果。坐标有缓存时跳过这一步。
- 解析必须兜底。结果文本里可能是 JSON,也可能是被包装过的文本;代码要能从
geocodes、return、pois里挑location,从route.paths[0]取距离时长,从forecasts或casts取天气序列。解析失败时退回正则。
高德对频率敏感:同一个会话的调用串行,起始间隔 0.36 秒,QPS 超限退避重试。这也是为什么「四个查询并发提交」不等于「四个请求同时发出」——它们只是同时进入队列。
规划里最容易被忽略的降级是路线失败:拿不到驾车结果时,用球面距离估算路网里程,避免整份行程退化成「目的地游」。天气拿不到时写「天气信息暂缺」;POI 不够时用内置的县域候选补足。
12. 12306 MCP 实战
一句话:先城市换站点码,再按站点查车次;查不到就用静态参考班次兜底。
12306 的工具更贴近业务:
| 工具 | 参数 | 用途 |
|---|---|---|
get-current-date | 无 | 取当前日期 |
get-station-code-of-citys | citys: "北京|上海" | 城市 → 站点码 |
get-station-code-by-names | stationNames: "北京南|上海虹桥" | 站名 → 站点码 |
get-tickets | date、fromStation、toStation、format、trainFilterFlags、limitedNum | 查车次 |
一次高铁方案大致是:如果没传出行日期,先取日期;再把城市转成站点码,然后用站点码查get-tickets,最后把班次附到行程里。它只在「高铁 + 到站租车」模式下触发,普通自驾不会调 12306。
12306 的失败并不罕见:官网限流、网络抖动、车次查不到,代码都按失败处理——查不到就退回内置的静态参考班次,并在结果里标明来源。静态班次不保证准确,但能保证页面不空;这是工程取舍。
两个 Server 都拿到了,下一节把它们串成一次规划。
13. 一次规划的流程
一句话:先取事实,再跑规则,边算边推;模型不在工具与规则的主链路上。
图 8:一次路书规划的完整时序
三个设计点:
1. 先事实,后规则。MCP 只把坐标、路线、天气、POI 取回来;逐日安排、预算、清单、适合谁,全部由规则函数从事实拼出来。这样数字可复算,改路时才能算清「多开了多少公里」。
2. 流式暴露过程。工具调用要几秒,页面不能白等。骨架先出,geo、route、weather、palette、itinerary依次推给前端。冷路径里route会推两次(取回路线后一次,规则算完里程后一次),itinerary通常两次(规则版、附加信息后的最终版),开启精修时再多一次。命中缓存时事件更少,palette会先于geo。
3. 缓存的是整份 trip。相同参数命中内存缓存时,工具调用全部跳过,只把缓存的阶段重放一遍。缓存没有持久化,进程重启即失效。
流程里模型只出现在两个位置,下一节展开。
14. 模型的两个位置
一句话:模型只做「把非结构化输入变成结构」,以及「可选地重写一版」;默认路径不参与算数字,精修是例外。
规划模块里,模型只有两个入口:
一、参数抽取。出发地或目的地缺失、且有一句话可解析时,才调模型从句子中抽origin、destination、days,要求只输出 JSON。字段齐了就直接用表单,不调模型。
二、可选精修。ROADBOOK_LLM_REFINE=1时,规则路书先生成、先推送,然后最多等 7 秒让模型合成一版;超时或异常就保留规则版。
第二点有个值得记住的教训:让模型输出结构化数据时,输出契约必须和消费方一致。精修提示词要的是完整行程 JSON,但里面没有budget、fit_verdict、checklist、pitfalls这些字段,代码也没有把模型结果和规则版合并。结果不是「文案变差」,而是数据缺字段——预算、必知、清单各自以占位、回退或残留旧状态的方式表现出来。同时,精修版里的distance、drive_hours也可能来自模型,不再是规则算出的值。
对话改路是另一个对照:意图解析不调模型,用正则识别天数、起终点、途经城、节奏、预算,城市拿CITY_COORDS校验。普通局部改动走规则;改起终点会重新进入规划流程,开启精修且未命中缓存时,仍可能调模型。
一句话总结:模型适合把非结构化输入变成结构,不适合生成你可以算出来的数字。
这些位置也带来新的故障模式。
15. 故障与边界
一句话:工程里的失败大多出在进程、超时、会话和契约上,而不是模型本身。
| 故障 | 现象 | 现有处理 | 边界 |
|---|---|---|---|
| 子进程冷启动 | 第一次调用慢几秒 | 启动时预热高德/Tavily | 12306 不预热,首次查班次慢 |
| 会话异常 | 某个 Server 一直失败 | 异常穿过上下文时摘除重建 | 被吞掉的错误不会触发淘汰 |
| 超时 | 页面等不到结果 | 精修 7 秒回退;12306 工具 14 秒后返回空 | 超时不等于取消,后台请求可能还在跑 |
| 旧响应 | 旧路书覆盖新结果 | 改路用请求序号(epoch),新规划用 AbortController | 都在前端,服务端无幂等键 |
| 限流 | 请求被拒 | 分钟限流、并发闸门、日配额 | 单进程内存计数,多实例不共享 |
| 契约漂移 | 精修后字段缺失 | 规则版兜底 | 没有 schema 校验与合并 |
本地跑通之后,才谈得上换远程。
16. 上远程 MCP 要补什么
一句话:本地换远程,第一件事不是改协议,而是补身份、密钥、隔离和运维。
| 要补的 | 本地 stdio | 远程 Streamable HTTP |
|---|---|---|
| 鉴权 | 无协议级鉴权;密钥经环境变量注入 | OAuth / Token |
| 密钥位置 | 本机 Server 进程 | 服务端,模型不可见 |
| 多用户 | 不涉及 | 租户隔离与配额 |
| 失败处理 | 冷启动、进程崩溃 | 网络超时、限流、重试 |
| 版本升级 | 跟 Host 走 | 独立部署,schema 要兼容 |
| 数据合规 | 本机 | 数据驻留与审计 |
什么时候该上远程:工具要共享、密钥要集中、用户跨设备、需要统一审计。什么时候本地就够:桌面应用、用户自己的 Key、需要读本机文件、追求低延迟。
这个项目选本地,是因为它本来就是单机应用:高德的 Key 应该在用户自己手里,放进 Server 子进程的环境变量,模型永远看不到。换成远程,第一件事是加一个代理,把密钥和多租户挡在模型之外。
最后是代码地图和决策清单。
17. 代码地图
| 文件 | 管什么 | 先看 |
|---|---|---|
agent.py | 模型决定调用的 agent | init_agent_service |
mcp_client.py | Client 侧:注册、会话、预热 | _build_servers、open_server_session |
desktop_txt_mcp.py | 30 行的自写 Server(见第 10 节) | count_desktop_txt_files |
travel_app.py | Host:路由与 SSE | plan_stream、routes |
trip_planner.py | 管线:工具调用与规则 | _gather_amap_staged、_build_fast_itinerary |
rail_hsr.py | 12306 工具与班次 | attach_hsr_rental_to_trip、fetch_hsr_trains |
static/js/roadbook-ui.js | 前端:SSE、地图、海报 | startPlan、handleStage |
阅读顺序
想跟一次完整调用,按这条线读:startPlan→/api/plan→plan_trip_async→_gather_amap_staged→open_server_session/call_tool→_build_fast_itinerary→handleStage。
本地运行
本地运行需要 Python 3.11+ 和可执行的npx:
gitclone https://github.com/devgxy/roadbook.gitcdroadbook python3-mvenv .venv&&source.venv/bin/activate pipinstall-rrequirements.txtcp.env.example .env# 至少填 AMAP_MAPS_API_KEYpython travel_app.py浏览器打开http://127.0.0.1:7861。算路要AMAP_MAPS_API_KEY,配图要TAVILY_API_KEY,参数抽取、可选精修和桌面助手要DASHSCOPE_API_KEY。
产品侧的说明在 product-design.md、roadbook-spec.md、interaction-ui.md。
18. 决策清单
全文小结:六个问题:
- 接工具用 MCP 还是直接 API?工具会被多个 Host 复用、需要标准发现与鉴权,选 MCP;一次性、无状态、只服务一个应用,直接 API 更简单。
- 本地还是远程?密钥属于用户、需要读本机文件、单机应用,选本地;要共享、要集中鉴权、要跨设备,选远程。
- Agent 还是管线?任务开放、工具集会变,选 agent;结果要可复算、要审计、流程固定,选管线。
- 模型还是规则?把非结构化输入变成结构,用模型;能从字段算出来的数字,用规则。
- 流式还是批量?工具耗时明显、用户需要感知进度,用 SSE;一次调用就返回,普通 JSON 即可。
- 模型输出怎么校验?结构必须和消费方一致;宁可回退规则版,也不要让缺字段的数据流进界面。
MCP 标准化了「工具怎么接」;但决定「谁来选工具、结果怎么用」的,始终是 Host 里的那套编排。