# 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` 3. 任意一个点解析模糊、缺少 `poi_id`、或地理结果交叉校验失败,直接返回错误 4. 只有在所有点都通过后,才把“预解析点位 JSON”交给 Agent 5. Agent 只负责候选顺序、逐段驾车计算、最佳路线选择、summary 和 warnings 6. 服务端最后再直接调用 `maps_schema_personal_map` 生成 deep link 7. 如果 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_http`、`http`、`sse` - 自定义 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 服务配置方式变化