Agent 速成笔记 · 第 13 章 智能旅行助手

Agent 速成笔记 · 第 13 章 智能旅行助手

源:Datawhale《Hello-Agents》第 13 章 | 定位:实战案例(多智能体 + 全栈工程)| 一句话:把"搜索景点 / 查天气 / 找酒店 / 整合行程"拆给四个专职智能体(Agent),用 MCP 接外部数据、用强类型模型锁定输出,再套一层前后端分离的 Web 应用。


0. 一章速览(30 秒)

  • 一句话:一个可运行的旅行规划产品 = 四层架构(前端 / 后端 / 智能体 / 外部服务)+ 四个专职 Agent + 一套 Pydantic 数据契约。
  • 本章解决什么问题:把前面学到的 Agent 范式、工具系统、MCP 协议串成一个真实可用、能被人直接使用的应用,并讲清"分工、契约、容错"三个工程决策。
  • 必须记住的 4 个点:
    • 多 Agent 分工的原则是按数据来源拆专家、最后留一个纯整合 Agent;整合 Agent 不调工具。
    • 单 Agent 的两个硬约束:SimpleAgent 每次 run() 只能调一个工具;ReactAgent 虽能多轮,但串行 LLM 调用把总耗时拉长。
    • MCP(Model Context Protocol)用 auto_expand=True 把一次注册变成 16 个工具;多 Agent 应共享同一个 MCP 实例。
    • 输出可用性的三件套:PlannerAgent 的 JSON 契约 + Pydantic 强类型 + field_validator 容错。

1. 项目概述与架构设计(13.1)

1.1 产品要解决什么

旅行规划的三个痛点:信息分散(景点、天气、酒店信息分散在不同站点,需要人工整合)、缺少个性化(通用攻略不考虑个人偏好、预算、时间)、难以调整(景点顺序、时间、预算相互关联,改一处要重排全局)。

目标形态:用户只输入"目的地 + 日期 + 偏好 + 预算 + 交通/住宿类型",系统产出含景点、餐饮、酒店、预算的完整行程。落地时要交付五项核心功能:

  • 智能行程规划:输入目的地、日期、偏好等,自动生成含景点、餐饮、酒店的完整计划;
  • 地图可视化:在地图上标注景点位置、绘制游览路线;
  • 预算计算:自动汇总门票、酒店、餐饮、交通费用并显示明细;
  • 行程编辑:支持增删、调整景点,并实时更新地图;
  • 导出功能:导出为 PDF 或图片,便于保存与分享。

这五项不是并列的功能清单,而是有依赖关系的:规划产出数据 → 地图/预算渲染 → 编辑改数据后重渲染 → 导出固化结果。后文的每个工程决策,基本都在回答"这五步里哪一步该由谁做"。

1.2 四层技术架构

系统采用前后端分离架构,分四层:

层 技术 职责
前端层 Vue3 + TypeScript 表单输入、结果展示、地图可视化
后端层 FastAPI API 路由、数据验证、业务逻辑
智能体层 HelloAgents 任务分解、工具调用、结果整合(4 个 Agent)
外部服务层 高德地图 API、Unsplash API、LLM API 提供数据与能力

数据流转链路:用户填写前端表单 → 后端验证数据 → 调用智能体系统 → 依次调用景点搜索、天气查询、酒店推荐、行程规划 Agent → 每个 Agent 通过 MCP 协议调用外部 API → 整合结果返回前端 → 前端渲染展示。

工程要点:智能体层不是孤岛,它被后端 Web 服务包裹,对外只暴露一个接口 POST /api/trip/plan。这是把 Agent 从"脚本 demo"变成"可被产品调用的服务"的关键一步。

1.3 运行形态(了解即可)

环境要求 Python 3.10+、Node.js 16+、npm 8+。后端跑在 8000 端口(uvicorn),前端跑在 5173 端口(Vite)。需要三类密钥:LLM API Key、高德地图 Web 服务 Key、Unsplash Access Key,统一放入 .env。


2. 数据模型设计(13.2):输出可控的地基

2.1 为什么需要数据模型

一次请求中,数据要穿过很长的链路:前端表单 → HTTP 请求 → 后端 Python 对象 → 外部 API 响应 → 后端 Python 对象 → HTTP 响应 → 前端 TypeScript 对象 → 页面展示。而且外部 API 的字段名并不统一——高德返回的坐标是 "116.397128,39.916527" 这样的字符串(要手动切分),Unsplash 可能用 longitude/latitude。

第一章原型用 Python 字典表示数据,有三个坑:

  • 字段名不统一:每个使用点都要手工处理差异。
  • 类型安全缺失:price 写成字符串 "60" 在 Python 中不会立即报错,算总预算时才出问题,且错误难定位。
  • 维护性差:加一个字段(比如 rating)要改多处,漏一处就数据不一致。

Pydantic 的解法是用类定义结构,自动完成验证、转换与序列化。

class Location(BaseModel):
    longitude: float = Field(..., description="经度", ge=-180, le=180)
    latitude: float = Field(..., description="纬度", ge=-90, le=90)

class Attraction(BaseModel):
    name: str = Field(..., description="景点名称")
    location: Location = Field(..., description="经纬度坐标")
    ticket_price: int = Field(default=0, ge=0, description="门票价格(元)")

这段是"基元 + 嵌套"的最小示范:Field(...) 里的 ... 表示必填,ge/le 是数值范围约束,把 Location 直接当 Attraction 的字段类型就是嵌套模型。

2.2 Pydantic 的核心概念

  • BaseModel:所有模型的基类,字段声明即"类型 + 默认值"。
  • Field:声明默认值、描述、验证规则。... 表示必填,创建对象时缺失会抛异常;给默认值即选填;ge/le/gt 是数值边界。
  • Optional / 默认值:Optional[str] = None 表示可选字段。
  • 嵌套与列表:模型可作字段类型(location: Location),也可用 List[Attraction];default_factory=list 提供空列表默认值。
  • field_validator(mode='before'):在类型校验之前改写原始值,专治外部 API 的脏格式。
class WeatherInfo(BaseModel):
    day_temp: int
    @field_validator('day_temp', mode='before')
    def parse_temperature(cls, v):
        if isinstance(v, str):
            v = v.replace('°C', '').replace('℃', '').replace('°', '').strip()
            try:
                return int(v)
            except ValueError:
                return 0        # 容错:解析不了就归零
        return v

高德返回的温度是 "16°C" 这类字符串,这个验证器把它转成整数;解析失败返回 0 而不是抛异常。原则是外部数据一律按不可信处理。

2.3 自底向上的模型层次

设计原则:先定义最基础的模型,再逐步组合成复杂结构,让每个模型都简单、好维护。

层 模型 关键字段
基元 Location longitude、latitude
元素 Attraction / Meal / Hotel 名称、地址、Location、费用
汇总 Budget 门票、酒店、餐饮、交通、总计
组合 DayPlan date、day_index、attractions、meals、hotel
顶层 TripPlan city、起止日期、days、weather_info、budget、建议

顶层 TripPlan 里,budget 是 Optional[Budget],weather_info 是列表——这两个"可空 / 可空列表"的设计,直接决定了前端必须做条件渲染。

另外注意 Location 用 ge/le 做了范围校验(经度 -180~180、纬度 -90~90),visit_duration 用 gt=0 保证为正。这类约束看似琐碎,实际作用是把非法数据挡在模型创建的瞬间:一旦外部 API 返回越界坐标或零时长,错误会立刻定位到字段,而不是在后续计算里变成难查的偏差。

2.4 模型在 Web 应用中的落地

FastAPI 中 Pydantic 模型可直接当请求与响应的类型定义:@app.post("/api/trip/plan", response_model=TripPlan),框架自动做三件事——验证请求数据、验证响应数据、生成 OpenAPI 文档。数据不合规会直接返回 400 并指出错在哪。

前端用 TypeScript interface 镜像同一套结构(可选字段用 ?),于是"后端返回什么、前端怎么用"完全对齐,IDE 还能提供补全与类型检查。

工程要点:前后端共用一套字段语义,是这类项目的第一道质量闸门;改字段时两边同时改,写错了在编译期或校验期就能被发现,而不是等到用户点开页面。


3. 多智能体协作设计(13.3):本章核心

3.1 为什么单 Agent 不行

单 Agent 要包办四件事:搜景点、查天气、搜酒店、整合行程。用 HelloAgents 里现有的两种范式都会撞墙:

方案 机制 问题
SimpleAgent 每次 run() 只执行一个工具 需多次调用,中间结果要手工传递,代码变复杂
ReactAgent 一次调用内多轮思考 + 多工具 每轮一次 LLM 调用,串行执行,总耗时长
单个复杂提示词 一个 Agent 包办四件事 难维护、易串味、出错难定位

单个大提示词有三个具体毛病:想改景点搜索逻辑(比如加评分筛选)会牵动整段提示词,容易影响其他部分;LLM 要同时理解四套要求,很容易搞混格式与参数;计划不符合预期时,无法判断是景点搜索不准、天气查询失败,还是整合逻辑有问题。

于是转向任务分解:把复杂任务拆成多个简单任务,不同 Agent 各司其职。原文的类比是现实中的旅行社——有景点顾问、酒店顾问,还有把一切汇总成行程的行程规划师,分工协作远比一个人全干高效。

3.2 四个 Agent 的角色设计

Agent 输入 工具 输出
AttractionSearchAgent(景点搜索) 城市 + 偏好 amap_maps_text_search 景点列表
WeatherQueryAgent(天气查询) 城市 amap_maps_weather 未来几天天气预报
HotelAgent(酒店推荐) 城市 + 住宿类型 amap_maps_text_search 酒店列表
PlannerAgent(行程规划) 用户需求 + 前三者输出 无 完整 TripPlan(JSON)

设计逻辑:前三个是信息获取型专家,各自只对接一类数据源;第四个是纯整合型专家,不调任何外部工具,只专注信息整合与行程编排。这个"3 个取数 + 1 个汇总"的骨架,是整章最可迁移的结构——它也为后续扩展(加餐厅推荐 Agent、交通规划 Agent)留好了位置。

3.3 提示词设计与输出契约

设计每个 Agent 的提示词时要先想清四件事:需要什么输入、应产生什么输出、要调什么工具、可能遇到什么问题。

取数型 Agent 的提示词追求"短且明确":写清工具调用格式、给出具体示例、强调必须用工具不得编造。

ATTRACTION_AGENT_PROMPT = """你是景点搜索专家。

**工具调用格式:**
`[TOOL_CALL:amap_maps_text_search:keywords=景点,city=城市名]`

**示例:**
- `[TOOL_CALL:amap_maps_text_search:keywords=景点,city=北京]`
- `[TOOL_CALL:amap_maps_text_search:keywords=博物馆,city=上海]`

**重要:**
- 必须使用工具搜索,不要编造信息
- 根据用户偏好({preferences})搜索{city}的景点
"""

提示词模板用 {preferences}、{city} 占位,运行时填充。示例行给出的是"该长什么样的调用",而"不要编造信息"这句是抑制幻觉的开关——它把模型从"凭记忆回答"推回"走工具取数"。

整合型 Agent 的提示词追求"强契约":直接给出完整 JSON 结构,并列出硬约束。

PLANNER_AGENT_PROMPT = """你是行程规划专家。
**输出格式:** 严格按照以下JSON格式返回:
{"city": "...", "start_date": "YYYY-MM-DD", "end_date": "YYYY-MM-DD",
 "days": [...], "weather_info": [...], "overall_suggestions": "...", "budget": {...}}
**规划要求:**
1. weather_info必须包含每天的天气
2. 温度为纯数字(不带°C)
3. 每天安排2-3个景点
4. 考虑景点距离和游览时间
5. 包含早中晚三餐
6. 提供实用建议
7. 包含预算信息
"""

第 2 条"温度为纯数字"是与数据模型约定好的双向契约:提示词要求模型输出纯数字,模型侧再用 field_validator 兜底——两道防线共同保证温度字段一定是整数。这就是"提示词契约 + 类型校验"配合的典型写法。

3.4 协作流程、查询构建与状态

编排是一条顺序流水线,共五步:

① attraction_agent.run("请搜索{city}的{preferences}景点")
② weather_agent.run("请查询{city}的天气")
③ hotel_agent.run("请搜索{city}的{accommodation}酒店")
④ planner_agent.run(_build_planner_query(...))    # 拼接用户需求 + 前三者输出
⑤ _parse_trip_plan(planner_response) → TripPlan

前三个 Agent 彼此独立,第四步把三份结果与用户需求拼成一段结构化提示词,第五步把返回的 JSON 解析成 Pydantic 对象。关键在于第四步的查询构建:

def _build_planner_query(self, request, attraction_response, weather_response, hotel_response) -> str:
    return f"""
请根据以下信息生成{request.city}的{request.days}日旅行计划:
**用户需求:**
- 目的地: {request.city}
- 日期: {request.start_date} 至 {request.end_date}
- 偏好: {request.preferences}  预算: {request.budget}
- 交通方式: {request.transportation}  住宿类型: {request.accommodation}
**景点信息:**
{attraction_response}
**天气信息:**
{weather_response}
**酒店信息:**
{hotel_response}
请生成详细的旅行计划,包括每天的景点安排、餐饮推荐、住宿信息和预算明细。
"""

用加粗小标题把"用户需求 / 景点 / 天气 / 酒店"切成独立段落,目的是让 LLM 分清信息边界、不串用。多 Agent 之间的信息传递就靠这段结构化拼接。

状态管理:整条链路的"状态"只有两处——入口的请求对象 TripPlanRequest 和出口的 TripPlan,中间结果是普通字符串。因为流程是单向顺序的,不需要引入复杂的共享状态机;这也意味着每一步的产物都可以被单独打印出来做调试。


4. MCP 工具集成(13.4)

4.1 为什么不直接调 API

高德 POI 搜索直接写 requests.get("https://restapi.amap.com/v3/place/text", params={...}) 看起来很直接,但有四个问题:

问题 说明
Agent 无法自主调用 Agent 靠识别 [TOOL_CALL:...] 标记调工具,写死函数就剥夺了自主决策
参数传递复杂 POI 搜索有 keywords/city/types/offset/page 等十余参数,全写进提示词极长
响应解析困难 返回 JSON 结构复杂,需手写解析;格式一变就要改代码
工具管理混乱 十几个 API 各写一个函数并手工注册,冗长且新增时改动多处

4.2 MCP 是什么、怎么接

MCP(Model Context Protocol) 是 Anthropic 提出的标准化协议,用于连接 LLM 与外部工具。本项目使用 amap-mcp-server(Node.js 实现)。高德自身提供了多个 API(POI 搜索、天气查询、路线规划等),经 MCP 封装后,Agent 侧统一表现为"一堆可调用工具",而不是"一堆要手写解析的 HTTP 接口"——这正是用 MCP 换来的收益。

mcp_tool = MCPTool(
    name="amap_mcp",
    command="npx",
    args=["-y", "@sugarforever/amap-mcp-server"],
    env={"AMAP_API_KEY": settings.amap_api_key},
    auto_expand=True
)
  • command / args 指定如何启动服务器:npx -y @sugarforever/amap-mcp-server 会从 npm 拉包并运行。
  • env 透传高德 API 密钥。
  • auto_expand=True 是最关键的开关:MCPTool 会自动查询服务器提供了哪些工具,并为每个工具创建一个独立的 Tool 对象。所以只注册一次,Agent 实际拿到 16 个工具(如 amap_maps_text_search、amap_maps_weather)。
  • 通信方式是进程间 stdin/stdout,不是 HTTP——更高效,也更容易管理。

原文特别说明:文档示例用 npx,本书代码仓实际用 uvx。二者设计理念一致,区别只在生态:npx 面向 JavaScript/Node.js(包来自 npm),uvx 面向 Python(包来自 PyPI),没有优劣之分。

4.3 一次工具调用的完整往返

Agent 生成 [TOOL_CALL:amap_maps_text_search:keywords=景点,city=北京]
  → 框架解析标记,取出工具名与参数
  → MCPTool 构造 JSON-RPC 消息,经 stdin 发给 MCP 服务器进程
       {"jsonrpc":"2.0","method":"tools/call",
        "params":{"name":"amap_maps_text_search","arguments":{"keywords":"景点","city":"北京"}}}
  → 服务器解析参数,调用高德 HTTP API,解析返回
  → 服务器把结果包成文本,经 stdout 回传
       {"jsonrpc":"2.0","result":{"content":[{"type":"text","text":"找到以下景点:..."}]}}
  → MCPTool 提取文本内容,作为工具输出交回 Agent

对 Agent 而言,它只需要知道"有一个叫 amap_maps_text_search 的工具可以搜景点",高德 API 的所有底层细节都被 MCP 协议与 MCPTool 封装了。

4.4 共享 MCP 实例

三个 Agent 都要用高德工具,那么各自建一个 MCPTool 还是共用?若各建一个,就会同时跑三个服务器进程、各自独立调高德 API,可能超出速率限制,还多占内存与 CPU。正确做法是在 TripPlannerAgent 的构造函数里只创建一个 MCPTool,再逐个 add_tool(self.mcp_tool) 分发给三个子 Agent:底层只有一个服务器进程,不仅省资源,也便于集中控制调用频率。

4.5 Unsplash 图片集成

景点配图用 Unsplash API,封装成 UnsplashService:base_url 为 https://api.unsplash.com,调 /search/photos,参数 query / per_page / client_id,超时 10 秒,异常时记日志并返回空列表。它没有被封装成 Tool 或 MCP 工具,而是直接在 API 路由里遍历景点补 image_url。

判断依据:图片搜索不需要 Agent 的智能决策,只是一步数据增强。只有当需求变成"让 Agent 自主决定是否需要图片、或用哪个图源"时,才值得把它工具化。

坑:Unsplash 是国外服务,也是少数可免费使用的图片 API,因此搜索结果可能不够准确;实际项目可考虑必应、百度或高德 POI 图片 API,但这些通常需要付费。


5. 前端开发要点(13.5)

5.1 前后端分离与单页应用

后端只提供 API、返回 JSON(核心接口就一个 POST /api/trip/plan);前端是 Vue3 + TypeScript 的单页应用(SPA):填表 → 发请求 → 等响应 → 渲染,全程页面不刷新。优势是前后端可独立开发、部署、测试,且同一套 API 能同时供 Web、移动端、桌面端使用。

5.2 类型定义与 API 服务层

types/index.ts 用 TypeScript interface 镜像后端 Pydantic 模型(可选字段用 ?),services/api.ts 用 Axios 封装调用:

const api = axios.create({
  baseURL: 'http://localhost:8000/api',
  timeout: 120000,            // 2 分钟超时
  headers: { 'Content-Type': 'application/json' }
})
export const generateTripPlan = async (request: TripPlanRequest): Promise<TripPlan> => {
  const response = await api.post<TripPlan>('/trip/plan', request)
  return response.data
}

为什么超时设 2 分钟:一次规划要串行调用多个 Agent,每个都要打 LLM 和外部 API,整体需 10-30 秒;超时太短请求会被误中断。函数签名里参数是 TripPlanRequest、返回值是 Promise<TripPlan>,TypeScript 会在两端都做检查。

5.3 结果页与地图可视化

Result 页面分五块:行程概览、预算明细、地图、每日行程、天气。地图用高德 JS API Loader 加载(version 2.0,zoom 12)后,遍历所有天的景点,为每个景点创建一个 Marker,位置取 attraction.location 的经纬度,label 显示序号。


6. 功能实现要点(13.6)

6.1 预算计算放后端

决策:预算在 PlannerAgent 侧算,不在前端算。理由是算预算所需的门票价、酒店价、餐饮标准,都是 PlannerAgent 生成行程时已经拿到的信息;前端重算等于复制一套逻辑,且更容易不准。做法是在 PlannerAgent 提示词里直接要求输出 budget 五项(total_attractions / total_hotels / total_meals / total_transportation / total),由 LLM 依据行程里的景点、酒店、餐饮安排估算。

原文给的估算口径很直观:若行程含故宫(门票 60 元)、天坛(15 元)、颐和园(30 元),则景点门票合计 105 元;3 天 2 晚、经济型酒店每晚 300 元,则酒店合计 600 元。也就是说,预算不是"另算一遍",而是行程生成过程的副产品——这也是它必须待在 PlannerAgent 里的根本原因。

前端展示时用 v-if="tripPlan.budget" 条件渲染——因为 budget 是可选字段,模型没给也不崩。这是"后端算、前端只展示"的典型分工。

6.2 加载进度条:模拟而非真实

流程耗时 10-30 秒,必须有反馈,否则用户会以为卡死、刷新或重复点击。实现是 setInterval 每 500ms 把进度 +10%,并在不同区间切换状态文案(搜索景点 → 查询天气 → 推荐酒店 → 生成行程计划),到 90% 封顶,成功后补到 100% 再跳转结果页。

注意这是模拟进度:前端无法准确知道后端的处理进度。它的价值是"让用户知道系统在工作",而不是精确反映真实阶段——真要做精确进度,需要后端把阶段状态推送出来。

6.3 行程编辑:状态管理是核心

编辑功能要同时维护两份状态:当前计划与原始计划。进入编辑模式时对当前计划做深拷贝存为原始副本(JSON.parse(JSON.stringify(...)))。为什么不能直接赋值?因为 JS 对象是引用类型,直接赋值会让两个变量指向同一对象,改一个会影响另一个。取消编辑则回滚到副本,保存修改则更新当前计划并重新初始化地图(景点位置可能已经变了)。

移动景点用 ES6 解构交换 [a[i], a[j]] = [a[j], a[i]](无需临时变量),删除景点用 splice(index, 1)。编辑模式下每个景点旁显示上移、下移、删除按钮。

6.4 导出:一个真实的技术坑

导出用 html2canvas(DOM → Canvas)+ jsPDF。坑:地图本身也是 Canvas 渲染的,而 html2canvas 处理嵌套 Canvas 存在兼容性问题,加上高德地图的 Canvas 渲染机制与跨域限制,"把地图 Canvas 转成图片再导出"并未完全解决问题。原文给出的替代方案:

  • 使用高德静态地图 API:调 maps_staticmap 生成静态地图图片,替代动态地图;
  • 分开导出:地图与行程内容分别导出,最后在后端合并;
  • 使用截图服务:用 Puppeteer 等无头浏览器在服务端截图;
  • 简化导出内容:导出时隐藏地图,只导文字内容。

最终采用简化方案 ④:导出时隐藏地图,只导行程文字与景点信息,保证功能可用。导出图片用 scale: 2(2 倍分辨率更清晰)、useCORS: true(允许跨域加载 Unsplash 图片);导出 PDF 时,因 A4 纸宽度为 210mm,需按 Canvas 宽高比算出对应高度,再 addImage 添加到 PDF。

这条坑的价值:外部渲染库与地图 SDK 的"Canvas 套 Canvas"是常见冲突点,遇到时优先选"换数据源(静态图)"或"绕开(隐藏/服务端截图)",而不是硬啃兼容性。

6.5 长页面导航

Result 内容很长,用侧边菜单 + 锚点跳转:菜单项的 key 对应各区块的 id,点击时调 scrollIntoView({ behavior: 'smooth', block: 'start' }),实现平滑滚动且顶部对齐。


7. 工程实践要点:这套架构怎么迁移到别的领域

本章骨架可以整体搬走,替换四类元素即可:

要替换的 旅行助手里的样子 迁移时怎么改
顶层数据模型 TripPlan 换成领域对象(购物清单/学习计划),仍自底向上组合
取数型 Agent 景点/天气/酒店 按外部数据源重新拆,一源一专家,提示词只讲本领域
整合型 Agent PlannerAgent 保留一个不调工具的汇总 Agent,输出 JSON 契约
工具接入 高德 MCP + Unsplash Service 有 MCP 就接 MCP;按"是否需智能决策"决定要不要工具化

判断清单(这些是可直接复用的决策依据):

  1. 拆分粒度看数据源,不看功能名:Agent 数量≈不同外部数据源数量 + 1(整合器)。
  2. 哪些该工具化:需要 Agent 临场决策的(搜什么关键词、要不要调)→ 工具/MCP;纯机械补全(如配图)→ 直接在服务层调用。
  3. 输出可取用的三道防线:整合 Agent 的强 JSON 契约 → Pydantic 强类型校验 → field_validator 容错脏数据。
  4. 计算逻辑放数据获取侧:谁手里有原始数据谁算(预算放 PlannerAgent),避免前端复制一套逻辑。
  5. 调试切入点:按 Agent 边界切开问题——计划不对时,先判断是景点取回得差、天气失败,还是整合提示词串了信息。
  6. 资源类共享:多个 Agent 共用的重型客户端(MCP 进程、LLM 客户端)只建一份,用 add_tool 分发。

8. 高频考点 & 易错点速查

  1. 四层架构与技术栈(Vue3+TS / FastAPI / HelloAgents / 高德 MCP + Unsplash + LLM)——常考"哪一层负责什么"。
  2. 数据模型自底向上的层次:Location → Attraction/Meal/Hotel/Budget → DayPlan → WeatherInfo → TripPlan。
  3. Field(...) 与给默认值的区别(必填 vs 可选);ge/le/gt 是数值约束。
  4. field_validator(mode='before') 的用途:在类型校验前改写外部脏数据("16°C" → 16),且必须有容错分支。
  5. 不选单 Agent 的两个硬约束:SimpleAgent 每次 run() 仅一个工具;ReactAgent 多轮 = 多次串行 LLM 调用、总耗时长。
  6. 四个 Agent 的分工逻辑:3 个取数专家 + 1 个不调工具的整合专家。
  7. 工具调用标记格式 [TOOL_CALL:tool:arg=value];提示词模板用 {占位符} 运行时填充。
  8. PlannerAgent 提示词第 2 条"温度纯数字"与 WeatherInfo 验证器是配套的双重保障——易混点。
  9. auto_expand=True 使一个 MCPTool 展开为 16 个工具;npx(JS/npm)与 uvx(Python/PyPI)只是生态不同。
  10. 共享 MCP 实例:三个 Agent 共用一个服务器进程,避免重复进程、超速率限制、多占资源。
  11. 为什么 Unsplash 不做成 Tool:无需智能决策,属数据增强步骤。
  12. 预算放后端的原因 + 前端对可选字段做条件渲染。
  13. 进度条是模拟的(500ms +10%,90% 封顶),不代表真实后端进度。
  14. 深拷贝用 JSON.parse(JSON.stringify());JS 引用类型直接赋值会共享对象。
  15. 导出与地图"嵌套 Canvas"的坑,及四种替代方案(静态地图 API / 分开导出 / 无头截图 / 隐藏地图)。

9. 与前后章节的衔接

本章是前面内容的"总装":Agent 范式(SimpleAgent / ReactAgent)来自智能体范式章节,工具系统与 MCP 协议来自工具与协议章节,多智能体协作思想来自多智能体章节。本章把这些串成一个真实应用,并补上此前没讲的两环——数据契约与全栈工程。它交付的是一套可复用模板:"取数专家 + 整合器 + 强类型契约"。后续章节要把单个 Agent 做得更强(记忆、评估、调优)时,都能直接嵌回这套骨架里。


10. 课后练习

在线试卷:https://md-quiz-online.app.workbuddy.host/

posted @ 2026-09-28 17:10  测试小罡  阅读(6)  评论(0)    收藏  举报