Files
geo-setp/docs/agent_design.md
2026-04-15 14:05:33 +08:00

10 KiB
Raw Permalink 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/load/plan
    • 负责 Authorization 鉴权与 HTTP 错误映射
  • schemas.py
    • 定义请求和响应模型
    • 实现输入校验
  • agent/route_plan.py
    • 路线规划 Agent
    • 负责 LLM、MCP、prompt、运行护栏、结果护栏
  • agent/load_plan.py
    • 装载规划 Agent
    • 负责装载模型、skills、工具接入与结构化输出
  • agent/load_plan_tools.py
    • 装载规划业务 API 工具
    • 当前提供车辆详情和未出货出货单查询
  • skills/fabric-load-planning/SKILL.md
    • 装载规划 skill
    • 定义面料判断、欠载与不混装规则
  • .env
    • 管理模型配置、MCP 配置和运行护栏配置

运行链路:

  1. 前端请求进入 FastAPI
  2. /route/plan/load/plan 先通过 Authorization 鉴权
  3. Pydantic 校验请求结构
  4. 根据不同入口进入对应 Agent
  5. Agent 调用地图 MCP 或装载业务 API 工具
  6. Agent 返回结构化结果
  7. 服务侧执行错误映射
  8. 返回 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 装载规划 Agent 接入

  • 装载规划 Agent 使用独立模型配置:
    • LOAD_PLAN_BASE_URL
    • LOAD_PLAN_API_KEY
    • LOAD_PLAN_MODEL
    • LOAD_PLAN_REQUEST_TIMEOUT_SECONDS
  • 装载规划业务 API 使用独立配置:
    • LOAD_PLAN_API_HOST
    • LOAD_PLAN_AGENT_ACCESS_KEY
    • LOAD_PLAN_API_TIMEOUT_SECONDS
  • 当前装载规划已接入两个业务工具:
    • 按车牌查询车辆详情
    • 按区域查询未出货出货单
  • 装载规则通过 pydantic-ai-skills 从本地 skills/ 目录注入

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 或命中模糊时直接失败
  • 已增加 /load/plan HTTP 入口
  • 已接入装载规划 Agent、业务 API tools 与本地 skills
  • 已验证 merchant_id + area + license_plate 可返回装载规划结果

当前尚未完成:

  • 把文档中的每一步拆成显式 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 服务配置方式变化