Files
geo-setp/docs/agent_design.md
2026-04-13 19:48:44 +08:00

9.1 KiB
Raw Blame History

Geo Agent 设计文档

1. 项目目标

本项目用于实现一个“多目标地点最优路线规划”服务。

核心能力如下:

  • 接收一个固定起点或当前位置语义的路线规划请求
  • 解析终点和多个途经点
  • 对候选送货顺序进行比较
  • 逐段调用高德地图 MCP 工具计算距离和时长
  • 根据策略选出最佳路线
  • 返回结构化 JSON供前端直接消费
  • 在需要时生成高德 deep link

当前阶段的目标不是构建一个复杂的调度系统,而是先交付一个可工作的第一版 Agent API。

2. 设计原则

  • Prompt-first第一版以 system_prompt 驱动整体执行流程,优先保留灵活性
  • Strong output contract最终输出必须符合固定的 Pydantic 结构
  • Guardrail-driven对输入、执行规模和输出一致性增加代码护栏
  • Stateless每次请求独立完成不依赖长期记忆
  • Tool-grounded坐标、POI、距离、时长、deep link 必须来自地图工具,而不是模型臆造

3. 架构概览

当前工程结构:

  • main.py
    • FastAPI 入口
    • 暴露 /healthz/route/plan
    • 负责 HTTP 错误映射
  • schemas.py
    • 定义请求和响应模型
    • 实现输入校验
  • agent.py
    • 负责 LLM、MCP、prompt、运行护栏、结果护栏
  • .env
    • 管理模型配置、MCP 配置和运行护栏配置

运行链路:

  1. 前端请求进入 FastAPI
  2. Pydantic 校验请求结构
  3. 服务侧执行预护栏检查
  4. Agent 运行,并由 system_prompt 驱动模型调用高德 MCP 工具
  5. Agent 返回结构化结果
  6. 服务侧执行结果护栏检查
  7. 返回 JSON 给前端

4. 关键技术选型与决策依据

4.1 为什么使用 pydantic_ai

  • 原生支持结构化输出
  • 可以直接挂接 MCP toolset
  • 非常适合“模型编排 + 强类型结果”场景
  • 与当前项目希望保留 prompt 灵活性的目标一致

4.2 为什么第一版采用 prompt-first

这次的核心业务不是纯算法问题,而是“模型理解任务 + 使用地图工具求事实 + 按规则组织返回”。

第一版采用 prompt-first 的原因:

  • 业务规则还在收敛阶段prompt 更容易快速调整
  • 高德 MCP 工具已经能直接提供底层能力,没有必要在第一版就再包一层 Python tool abstraction
  • 当前最重要的是先建立“可靠结果 + 清晰约束 + 可持续迭代”的骨架

4.3 为什么不是纯 prompt-only

虽然第一版以 system_prompt 为核心,但没有把全部控制权放给模型。原因是以下几类问题更适合代码层兜底:

  • 输入明显非法
  • 候选排列爆炸
  • 输出结构不一致
  • 运行配置错误

因此最终方案是:

  • Prompt 负责业务编排
  • Pydantic 负责结构约束
  • Python 代码负责运行护栏

5. 当前实现策略

5.1 LLM 和 MCP 接入

  • LLM豆包 Ark OpenAI-compatible 接口
  • MCP高德远程 streamable_http
  • MCP 已验证可连通,当前可用工具包括:
    • maps_geo
    • maps_text_search
    • maps_search_detail
    • maps_direction_driving
    • maps_distance
    • maps_schema_personal_map
    • maps_schema_navi
    • maps_weather

5.2 Prompt 组织方式

system_prompt 中定义了以下行为约束:

  • 必须显式调用地图工具
  • 必须逐段计算驾车路线
  • 必须区分 fixed origin 和 current location
  • 必须按 route_strategy 做选择
  • 不得伪造坐标、POI、距离、时长和 deep link
  • 必须输出结构化结果

在实际运行时,还会再注入一次“用户级 prompt”其中包含

  • 当前请求 JSON
  • 本次候选顺序的硬上限
  • 对起点模式的额外提醒

这样做的目的是把“长期规则”和“本次任务上下文”拆开,减少 prompt 污染。

5.3 当前严格执行流

当前版本已不再把“地址解析是否足够精确”和“deep link 是否能生成”完全交给模型决定。

当前执行流如下:

  1. 服务端先对起点、终点、所有途经点做前置解析
  2. 每个点都必须成功完成:
  • maps_text_search 命中精确 POI
  • maps_search_detail 返回稳定坐标
  • 取得 poi_id
  1. 任意一个点解析模糊、缺少 poi_id、或地理结果交叉校验失败,直接返回错误
  2. 只有在所有点都通过后,才把“预解析点位 JSON”交给 Agent
  3. Agent 只负责候选顺序、逐段驾车计算、最佳路线选择、summary 和 warnings
  4. 服务端最后再直接调用 maps_schema_personal_map 生成 deep link
  5. 如果 deep link 生成失败,则整个请求失败

6. 已实现的代码护栏

6.1 输入护栏

schemas.py 中已经实现:

  • 清理空白字符串
  • stop.address 不能为空
  • stops 列表不能为空
  • destination_address 不能为空
  • origin_mode=fixed 时必须提供 origin_address
  • 终点地址不能同时出现在 stops
  • max_permutations 如果传入,必须大于 0
  • need_deep_link 必须为 true

6.2 执行规模护栏

agent.py 中已经实现:

  • .env 读取 ROUTE_MAX_PERMUTATIONS
  • 当前默认值是 20
  • 请求可以传更小值,但不能超过服务配置上限
  • 在模型执行前,根据 stops 数量计算排列数 n!
  • 如果排列数超过限制,直接报错并返回 422

当前策略是“超限直接拒绝”,暂不做启发式近似。

6.3 结果护栏

agent.py 中已经实现:

  • 输出 origin_mode 必须和请求一致
  • resolved_destination.role 必须是 destination
  • resolved_stops 数量必须和输入一致
  • 所有 resolved_stops.role 都必须是 stop
  • origin_mode=fixed 时必须返回 resolved_origin
  • origin_mode=current_location 时禁止返回固定 resolved_origin
  • 成功结果必须至少有一个 candidate
  • best_route 必须能在 candidates 中找到对应项
  • success 必须为 true
  • 成功结果必须包含至少一个 deep link
  • 成功结果中的所有点必须带有 poi_id

6.4 配置护栏

已实现以下配置检查:

  • 必须提供模型 URL、API key、模型名
  • MCP URL 必须存在
  • MCP transport 仅允许 streamable_httphttpsse
  • 自定义 MCP 认证头必须成对提供
  • ROUTE_MAX_PERMUTATIONS 必须是正整数
  • 模型和 MCP 的 timeout 参数必须是正数

6.5 超时护栏

已实现以下超时相关策略:

  • 模型请求超时通过 ARK_REQUEST_TIMEOUT_SECONDS 配置
  • MCP 连接超时通过 AMAP_MCP_TIMEOUT_SECONDS 配置
  • MCP 读取超时通过 AMAP_MCP_READ_TIMEOUT_SECONDS 配置
  • 上游 timeout 会被映射为 HTTP 504而不是泛化成 500

7. 当前已实施进展

当前已完成:

  • FastAPI 服务可启动
  • /healthz 正常返回 200
  • /route/plan 已打通真实调用链
  • 高德远程 MCP 连通性已验证
  • 单个途经点请求可成功返回结构化结果
  • 超限请求可返回 422并中止模型执行
  • 已改为严格 deep-link 模式:成功结果必须包含 deep link否则直接失败
  • 已增加前置点位解析与 POI 校验阶段,缺少 poi_id 或命中模糊时直接失败

当前尚未完成:

  • 把文档中的每一步拆成显式 Python 服务函数
  • current_location 场景的更细粒度行为控制
  • 深链策略的更强一致性校验
  • 自动化测试
  • 前端展示层 HTML 输出

8. 关键取舍

8.1 已做取舍

  • 选择 prompt-first而不是一开始就把所有流程写死在 Python 中
  • 选择保留高德 MCP 原生工具,而不是先做二次工具封装
  • 选择超限直接报错,而不是第一版就引入启发式近似搜索
  • 选择结构校验和运行护栏优先,而不是先追求功能面最大化

8.2 这套取舍的收益

  • 业务规则可快速迭代
  • 系统仍然保有基本可控性
  • 接口结构对前端稳定
  • 便于后续逐步从 prompt-first 演进到“prompt + service function”混合架构

8.3 当前代价

  • 模型仍然承担了较多流程理解责任
  • 某些复杂场景的结果稳定性暂时依赖 prompt 质量
  • 当前没有启发式优化,超限就会拒绝
  • 当前上游调用仍然依赖外部服务稳定性,但 timeout 已具备可调能力和明确错误语义

9. 已知限制

  • 当前版本的最优路线能力仍然高度依赖 LLM 遵守 prompt
  • 当前对 best_route 的一致性校验是结构级护栏,不是全量数学证明
  • current_location 模式尚未做专门增强
  • need_html 目前尚未实现独立展示层
  • 没有缓存机制,请求成本与工具调用次数直接相关
  • 当前严格策略可能会拒绝一部分“人看上去可接受、但程序判断为不够精确”的地址输入

10. 下一阶段 TODO

  • current_location 模式增加专门提示和更严格结果校验
  • deep_link_mode 增加更明确的输出校验规则
  • 追加自动化测试,覆盖成功路径和失败路径
  • 将部分高频子流程下沉为显式服务函数
  • 评估是否引入启发式近邻策略处理超限请求
  • 增加请求日志和运行观测能力
  • 增加 README 中的启动与调试说明

11. 文档维护建议

后续如果发生以下变更,应同步更新本文档:

  • Prompt 主策略变化
  • 输出结构变化
  • 错误语义变化
  • 护栏策略变化
  • MCP 服务配置方式变化