业务数据对话查询智能体系统
文档信息
| 项目 | 内容 |
|---|---|
| 文档版本 | v2.0 |
| 编制日期 | 2026-07-06 |
| 项目名称 | 业务数据对话查询智能体系统 |
| 技术栈 | FastAPI + LangGraph + Redis |
| 文档状态 | 定稿 |
一、项目概述
1.1 项目背景
本项目旨在为传统业务系统构建一个基于自然语言对话的数据查询智能体系统。用户通过对话界面提出与业务系统相关的问题(如设备状态、历史记录、统计报告等),系统自动理解意图、调用后端 API 接口、并将结果以自然语言形式返回给用户。
适用场景:无人机数据查询、工单系统、设备巡检、物流追踪等任何具备 RESTful API 的传统业务系统,均可基于本框架快速构建对话查询智能体。
1.2 项目目标
| 阶段 | 目标 | 时间 |
|---|---|---|
| 阶段一 | 实现基础的对话查询功能,支持 5-8 个核心业务数据查询意图 | 2-3 周 |
| 阶段二 | 完善多轮对话记忆管理,优化指代消解准确率至 95% 以上 | 4-6 周 |
| 阶段三 | 扩展智能体能力,接入 OCR 等更多工具,实现复杂任务编排 | 长期 |
1.3 设计原则
- 模块化:各层职责单一,便于独立演进和替换
- 可观测:全链路日志、追踪和监控
- 成本可控:通过分层记忆设计,控制 LLM Token 消耗
- 扩展预留:为后续引入多智能体协作预留架构接口
- 稳定优先:生产环境依赖成熟技术,新技术仅用于预研
二、技术选型
2.1 技术栈总览
| 层级 | 技术选型 | 选型理由 |
|---|---|---|
| Web 框架 | FastAPI | 高性能异步,原生支持 OpenAPI 文档,与 Python AI 生态无缝集成 |
| API 网关 | Higress | 阿里云开源云原生网关,统一管理入口流量和存量业务 API;内置限流/鉴权/熔断/AI 插件 |
| AI 编排引擎 | LangGraph | 有状态工作流编排,支持分支、循环和持久化检查点,内置 ToolNode、消息管理 |
| 记忆存储 | Redis | 低延迟会话存储,支持 ChatMessageHistory 和状态快照 |
| 长期记忆 | PostgreSQL | 用户偏好(L3a)+ pgvector 语义检索(L3b),阶段二启用 |
| 可观测性 | LangSmith | LLM 调用链路追踪、成本监控和调试 |
| 大模型 | DeepSeek / Qwen | 通过统一 API 接入,支持多模型切换 |
| API 集成 | httpx / aiohttp | 直接 HTTP 调用 Apifox 业务接口(经 Higress 路由) |
2.2 为什么选择这套技术栈
FastAPI + LangGraph 是当前构建生产级 AI 智能体服务的事实标准组合。
- FastAPI 提供高性能异步能力,与 Python AI 生态无缝集成
- LangGraph 通过图(Graph)的方式定义智能体工作流,将推理、工具调用、状态管理以节点和边的形式显式表达,使复杂的多步骤流程变得可控、可调试、可持久化。LangGraph 已内置 ToolNode、消息管理、Checkpointer 等核心能力,无需额外依赖 LangChain
- 直接 HTTP 调用 Apifox API ------ 经过 Higress 统一路由和管理后调用,比依赖 MCP Server 更稳定、可控,避免了新协议在生产环境中的不确定性
- Higress 云原生网关 ------ 阿里云开源的基于 Istio + Envoy 的 API 网关,同时承担两个角色:
- 入口网关:替代自建认证/限流/熔断,提供开箱即用的生产级网关能力
- API 管理平面:统一管理存量 Apifox 业务接口的路由、版本、鉴权、限流,避免在应用代码中零散管理数十个 API 端点
2.3 环境依赖
bash
# 核心依赖
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
langgraph>=0.2.0 # AI 编排引擎,内置 ToolNode/消息管理/Checkpointer
langgraph-checkpoint-redis>=0.1.0 # Redis Checkpointer
redis>=5.0.0
httpx>=0.27.0
pydantic>=2.0.0
pydantic-settings>=2.0.0
# 可观测性
langsmith>=0.1.0
# 阶段二启用
# asyncpg>=0.30.0 # PostgreSQL 长期记忆
# sqlalchemy>=2.0.0
三、系统架构设计
3.1 分层逻辑架构
#mermaid-svg-eS1cqcFqSXM7OCZg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eS1cqcFqSXM7OCZg .error-icon{fill:#552222;}#mermaid-svg-eS1cqcFqSXM7OCZg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eS1cqcFqSXM7OCZg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eS1cqcFqSXM7OCZg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eS1cqcFqSXM7OCZg .marker.cross{stroke:#333333;}#mermaid-svg-eS1cqcFqSXM7OCZg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eS1cqcFqSXM7OCZg p{margin:0;}#mermaid-svg-eS1cqcFqSXM7OCZg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster-label text{fill:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster-label span{color:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster-label span p{background-color:transparent;}#mermaid-svg-eS1cqcFqSXM7OCZg .label text,#mermaid-svg-eS1cqcFqSXM7OCZg span{fill:#333;color:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg .node rect,#mermaid-svg-eS1cqcFqSXM7OCZg .node circle,#mermaid-svg-eS1cqcFqSXM7OCZg .node ellipse,#mermaid-svg-eS1cqcFqSXM7OCZg .node polygon,#mermaid-svg-eS1cqcFqSXM7OCZg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eS1cqcFqSXM7OCZg .rough-node .label text,#mermaid-svg-eS1cqcFqSXM7OCZg .node .label text,#mermaid-svg-eS1cqcFqSXM7OCZg .image-shape .label,#mermaid-svg-eS1cqcFqSXM7OCZg .icon-shape .label{text-anchor:middle;}#mermaid-svg-eS1cqcFqSXM7OCZg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eS1cqcFqSXM7OCZg .rough-node .label,#mermaid-svg-eS1cqcFqSXM7OCZg .node .label,#mermaid-svg-eS1cqcFqSXM7OCZg .image-shape .label,#mermaid-svg-eS1cqcFqSXM7OCZg .icon-shape .label{text-align:center;}#mermaid-svg-eS1cqcFqSXM7OCZg .node.clickable{cursor:pointer;}#mermaid-svg-eS1cqcFqSXM7OCZg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eS1cqcFqSXM7OCZg .arrowheadPath{fill:#333333;}#mermaid-svg-eS1cqcFqSXM7OCZg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eS1cqcFqSXM7OCZg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eS1cqcFqSXM7OCZg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eS1cqcFqSXM7OCZg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eS1cqcFqSXM7OCZg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eS1cqcFqSXM7OCZg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster text{fill:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg .cluster span{color:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eS1cqcFqSXM7OCZg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eS1cqcFqSXM7OCZg rect.text{fill:none;stroke-width:0;}#mermaid-svg-eS1cqcFqSXM7OCZg .icon-shape,#mermaid-svg-eS1cqcFqSXM7OCZg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eS1cqcFqSXM7OCZg .icon-shape p,#mermaid-svg-eS1cqcFqSXM7OCZg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eS1cqcFqSXM7OCZg .icon-shape .label rect,#mermaid-svg-eS1cqcFqSXM7OCZg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eS1cqcFqSXM7OCZg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eS1cqcFqSXM7OCZg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eS1cqcFqSXM7OCZg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-eS1cqcFqSXM7OCZg .user>*{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .user span{fill:#e1f5fe!important;stroke:#01579b!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .gateway>*{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .gateway span{fill:#fff9c4!important;stroke:#fbc02d!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .core>*{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .core span{fill:#e8f5e9!important;stroke:#2e7d32!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .memory>*{fill:#fce4ec!important;stroke:#c62828!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .memory span{fill:#fce4ec!important;stroke:#c62828!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .infra>*{fill:#f3e5f5!important;stroke:#6a1b9a!important;stroke-width:2px!important;}#mermaid-svg-eS1cqcFqSXM7OCZg .infra span{fill:#f3e5f5!important;stroke:#6a1b9a!important;stroke-width:2px!important;} 基础设施与工具层
记忆与状态层
智能体核心编排层
Higress 统一网关
用户接入层
认证通过
读取L2注入上下文
复杂任务
简单任务
HTTP 调用
路由/限流/重试
返回数据
提取实体
写入历史
读写
读写偏好
调用
监控埋点
监控埋点
网关指标
Web / App / API 客户端
入口网关
鉴权 / 限流 / 路由
API 管理平面
存量业务接口统一治理
预处理中间件
指代消解 / 实体注入
意图路由器
LLM分类 + 规则兜底
Plan-Execute 执行器
预留扩展
语义工具适配器
Pydantic Schema转换
状态更新器
实体提取与持久化
L1 对话缓冲
Redis List
L2 实体状态快照
Redis Hash
L3 长期记忆
PostgreSQL + pgvector
大语言模型
DeepSeek/Qwen
业务 API
业务数据接口
可观测性平台
LangSmith
3.2 各层职责说明
| 层级 | 职责 | 关键组件 |
|---|---|---|
| 用户接入层 | 提供 Web/App/API 聊天界面 | 前端聊天组件、RESTful API |
| Higress 统一网关 | 入口鉴权限流 + 存量 API 治理 | 入口路由、JWT 认证、Rate Limiter、API 版本管理、熔断 |
| 智能体编排层 | 核心智能体逻辑 | 预处理、意图路由、工具调用、状态更新 |
| 记忆与状态层 | 三层记忆存储 | Redis(L1/L2)、PostgreSQL+pgvector(L3) |
| 基础设施层 | 外部依赖 | LLM、Apifox API、可观测性平台 |
3.3 核心交互时序
状态更新器 业务接口(Apifox) 语义工具适配器 大模型 意图路由器 记忆层(Redis) 预处理中间件 Higress 网关 状态更新器 业务接口(Apifox) 语义工具适配器 大模型 意图路由器 记忆层(Redis) 预处理中间件 Higress 网关 #mermaid-svg-jnMCGtPWnp2LUy4s{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jnMCGtPWnp2LUy4s .error-icon{fill:#552222;}#mermaid-svg-jnMCGtPWnp2LUy4s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jnMCGtPWnp2LUy4s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jnMCGtPWnp2LUy4s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jnMCGtPWnp2LUy4s .marker.cross{stroke:#333333;}#mermaid-svg-jnMCGtPWnp2LUy4s svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jnMCGtPWnp2LUy4s p{margin:0;}#mermaid-svg-jnMCGtPWnp2LUy4s .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jnMCGtPWnp2LUy4s text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jnMCGtPWnp2LUy4s .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jnMCGtPWnp2LUy4s .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jnMCGtPWnp2LUy4s #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jnMCGtPWnp2LUy4s .sequenceNumber{fill:white;}#mermaid-svg-jnMCGtPWnp2LUy4s #sequencenumber{fill:#333;}#mermaid-svg-jnMCGtPWnp2LUy4s #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jnMCGtPWnp2LUy4s .messageText{fill:#333;stroke:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jnMCGtPWnp2LUy4s .labelText,#mermaid-svg-jnMCGtPWnp2LUy4s .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .loopText,#mermaid-svg-jnMCGtPWnp2LUy4s .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jnMCGtPWnp2LUy4s .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jnMCGtPWnp2LUy4s .noteText,#mermaid-svg-jnMCGtPWnp2LUy4s .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jnMCGtPWnp2LUy4s .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jnMCGtPWnp2LUy4s .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jnMCGtPWnp2LUy4s .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jnMCGtPWnp2LUy4s .actorPopupMenu{position:absolute;}#mermaid-svg-jnMCGtPWnp2LUy4s .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jnMCGtPWnp2LUy4s .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jnMCGtPWnp2LUy4s .actor-man circle,#mermaid-svg-jnMCGtPWnp2LUy4s line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jnMCGtPWnp2LUy4s :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户 "它现在的飞行高度是多少?" 1 JWT 鉴权 + 限流 2 转发请求 3 查询L2实体层 4 {drone_id: "DJI-7890"} 5 注入 System Prompt 6 转发增强Prompt 7 意图分类 8 QueryAltitude(drone_id=DJI-7890) 9 调用工具 10 GET /v1/drones/position?id=DJI-7890 11 API 路由 / 限流 / 重试 12 转发请求 13 {altitude:150.5, unit:"m"} 14 返回数据 15 数据清洗与格式化 16 传递返回数据 17 更新L2层TTL 18 完成 19 "当前飞行高度150.5米" 20 用户
四、核心模块详细设计
说明 :以下代码示例以无人机数据查询为具体业务场景。本框架适用于任何具备 RESTful API 的传统业务系统,只需替换工具适配器中的 API 调用和意图路由中的 Prompt 即可适配新场景。
4.1 三层记忆架构
这是整个系统保证多轮对话连贯性的核心设计。
| 层级 | 存储介质 | 数据结构 | 生命周期 | 核心用途 |
|---|---|---|---|---|
| L1 对话缓冲 | Redis List | 消息列表(最近 N 条) | 单次会话,TTL 30 分钟 | 存储对话原文,供 LLM 理解上下文 |
| L2 实体状态快照 | Redis Hash | 结构化键值对 | 单次会话,TTL 2 小时 | 核心创新:存储当前关注的业务实体 ID、最近操作等关键实体 |
| L3a 结构化配置 | PostgreSQL | 关系表 | 跨会话永久 | 用户偏好、常用机队配置 |
| L3b 语义记忆 | pgvector | 向量索引 | 跨会话永久 | 历史对话摘要向量化 → 相似检索 → RAG 辅助回答 |
L2 实体状态数据结构示例(支持多实体追踪):
json
{
"user_id": "zhangsan",
"active_entities": [
{"type": "drone", "id": "DJI-7890", "last_referenced": "2026-07-06T14:30:00Z"},
{"type": "drone", "id": "DJI-1234", "last_referenced": "2026-07-06T14:25:00Z"}
],
"focus_entity": {"type": "drone", "id": "DJI-7890"},
"last_action": "query_position",
"last_action_time": "2026-07-06T14:30:00Z"
}
设计要点 :
active_entities支持追踪上下文中多个活跃实体(如用户说"比较一下 DJI-7890 和 DJI-1234 的电量"),focus_entity指向最近提及的实体用于指代消解。
实现代码骨架:
python
# src/memory/l2_entity.py
import json
from typing import Optional, List, Dict, Any
from datetime import datetime
from redis.asyncio import Redis
class EntityStateManager:
"""L2 实体状态管理器 ------ 支持多实体追踪"""
def __init__(self, redis_client: Redis):
self.redis = redis_client
self.ttl = 7200 # 2小时
self.max_entities = 10 # 最多追踪的活跃实体数
def _key(self, session_id: str, user_id: str) -> str:
"""Key 格式包含 user_id 隔离,防止会话状态泄露"""
return f"user:{user_id}:session:{session_id}:entity"
async def get(self, session_id: str, user_id: str) -> dict:
"""获取完整实体状态"""
data = await self.redis.hgetall(self._key(session_id, user_id))
result = {k.decode(): v.decode() for k, v in data.items()}
# 反序列化 JSON 字段
if "active_entities" in result:
result["active_entities"] = json.loads(result["active_entities"])
if "focus_entity" in result:
result["focus_entity"] = json.loads(result["focus_entity"])
return result
async def update(self, session_id: str, user_id: str, updates: dict):
"""批量更新实体状态"""
key = self._key(session_id, user_id)
# 序列化 JSON 字段
serialized = {}
for k, v in updates.items():
if isinstance(v, (list, dict)):
serialized[k] = json.dumps(v, ensure_ascii=False)
else:
serialized[k] = str(v)
await self.redis.hset(key, mapping=serialized)
await self.redis.expire(key, self.ttl)
async def get_focus_drone(self, session_id: str, user_id: str) -> Optional[str]:
"""获取当前焦点无人机 ID(用于指代消解)"""
state = await self.get(session_id, user_id)
focus = state.get("focus_entity")
if focus and focus.get("type") == "drone":
return focus["id"]
return None
async def track_entity(self, session_id: str, user_id: str,
entity_type: str, entity_id: str):
"""追踪一个实体,更新活跃列表和焦点"""
state = await self.get(session_id, user_id)
entities: List[Dict] = state.get("active_entities", [])
# 移除同 ID 的旧记录
entities = [e for e in entities
if not (e["id"] == entity_id and e["type"] == entity_type)]
# 插入到列表头部(最近活跃)
entities.insert(0, {
"type": entity_type,
"id": entity_id,
"last_referenced": datetime.utcnow().isoformat()
})
# 限制最大追踪数量
entities = entities[:self.max_entities]
await self.update(session_id, user_id, {
"active_entities": entities,
"focus_entity": {"type": entity_type, "id": entity_id}
})
L3 长期记忆 ------ 双路径分层设计(阶段二启用):
⚠️ L3 在阶段一暂不实现,阶段一用 Redis 覆盖全部记忆需求。以下为阶段二预留设计。
L3 按数据类型拆分为两条独立路径,各用其所长:
| 路径 | 存储 | 数据类型 | 查询方式 | 典型场景 |
|---|---|---|---|---|
| L3a 结构化配置 | PostgreSQL | 用户偏好、机队配置 | 主键精确查询 | "我的默认单位是英制吗?" |
| L3b 语义记忆 | pgvector(PG 扩展) | 历史对话摘要向量 | 余弦相似度检索 | "上次那台出故障的设备后来怎么样了?" |
为什么用 pgvector 而不是独立向量库?
- PostgreSQL 已在技术栈中,
CREATE EXTENSION vector一行命令即可启用 - 零额外运维成本,备份/高可用复用现有 PG 方案
- 数据量预估 < 100 万条,pgvector 的 IVFFlat/HNSW 索引完全够用
- 结构化查询 + 向量检索可在同一条 SQL 中完成,无需跨库 Join
L3a 结构化配置实现:
python
# src/memory/l3_long_term.py
from sqlalchemy import Column, String, JSON, DateTime
from sqlalchemy.ext.declarative import declarative_base
from datetime import datetime
Base = declarative_base()
class UserPreference(Base):
__tablename__ = "user_preferences"
user_id = Column(String, primary_key=True)
default_fleet = Column(JSON, default=[])
preferred_units = Column(String, default="metric") # metric / imperial
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
L3b 语义记忆实现:
python
# src/memory/l3b_semantic.py
import json
from typing import List, Optional
from datetime import datetime
from openai import AsyncOpenAI
from sqlalchemy import Column, String, Text, DateTime, Float
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.ext.declarative import declarative_base
from pgvector.sqlalchemy import Vector
Base = declarative_base()
class ConversationSummary(Base):
"""对话摘要表 ------ 存储向量化的历史对话摘要"""
__tablename__ = "conversation_summaries"
id = Column(String, primary_key=True) # UUID
user_id = Column(String, index=True, nullable=False)
session_id = Column(String, index=True)
summary_text = Column(Text, nullable=False) # 对话摘要原文
embedding = Column(Vector(1536)) # OpenAI text-embedding-3-small 向量
metadata_ = Column("metadata", JSON, default={}) # 涉及的无人机ID、意图类型等
created_at = Column(DateTime, default=datetime.utcnow)
class SemanticMemoryManager:
"""L3b 语义记忆管理器"""
def __init__(self, db_session, embedding_client: AsyncOpenAI,
model: str = "text-embedding-3-small"):
self.db = db_session
self.client = embedding_client
self.model = model
async def _embed(self, text: str) -> List[float]:
"""文本向量化"""
resp = await self.client.embeddings.create(
model=self.model, input=text
)
return resp.data[0].embedding
async def store_summary(self, user_id: str, session_id: str,
summary: str, metadata: dict) -> str:
"""存储对话摘要及其向量"""
import uuid
summary_id = str(uuid.uuid4())
embedding = await self._embed(summary)
record = ConversationSummary(
id=summary_id,
user_id=user_id,
session_id=session_id,
summary_text=summary,
embedding=embedding,
metadata_=metadata,
)
self.db.add(record)
await self.db.commit()
return summary_id
async def search_similar(self, user_id: str, query: str,
top_k: int = 3) -> List[dict]:
"""语义检索:找出与当前问题最相似的历史对话摘要"""
query_embedding = await self._embed(query)
# pgvector 余弦相似度检索
results = await self.db.execute(
"""
SELECT summary_text, metadata,
1 - (embedding <=> :embedding) AS similarity
FROM conversation_summaries
WHERE user_id = :user_id
ORDER BY embedding <=> :embedding
LIMIT :top_k
""",
{"embedding": query_embedding, "user_id": user_id, "top_k": top_k}
)
return [
{"summary": r.summary_text, "metadata": r.metadata, "score": r.similarity}
for r in results
]
L3b 的 RAG 使用流程:
用户提问: "上次那台报修的设备现在什么状态?"
│
├─→ L2 指代消解: "那台" → "DEV-7890"
│
├─→ L3b 语义检索: 搜索 "DEV-7890 故障" 相关历史摘要
│ └─→ 命中: ["DEV-7890 于 7月3日报告故障,已提交维修工单 #4521"]
│
└─→ 将召回的历史上下文注入 System Prompt → LLM 生成回答
4.2 预处理中间件(指代消解)
在请求进入 LLM 之前,拦截并处理指代消解,将"它"、"那架"等模糊指代替换为明确的实体 ID。
python
# src/middleware/reference.py
from typing import List
import re
class ReferenceResolutionMiddleware:
"""指代消解预处理中间件"""
REFERENCE_PATTERNS: List[str] = ["它", "那架", "上一架", "这架", "当前"]
def __init__(self, entity_manager):
self.entity_manager = entity_manager
async def process(self, user_input: str, session_id: str, user_id: str) -> str:
# 检测是否包含指代词
has_reference = any(p in user_input for p in self.REFERENCE_PATTERNS)
if has_reference:
# 获取焦点无人机
drone_id = await self.entity_manager.get_focus_drone(session_id, user_id)
if drone_id:
# 将指代信息注入到输入中
return f"[当前操作无人机: {drone_id}]\n{user_input}"
return user_input
4.3 意图路由器
采用 LLM 分类 + 规则兜底 的双重策略,确保高准确率和高可用性。
python
# src/core/router.py
from typing import Optional, Literal
from pydantic import BaseModel, Field
from openai import AsyncOpenAI
class IntentClassification(BaseModel):
"""意图分类结果"""
intent: Literal[
"query_position", # 查询实时位置
"query_status", # 查询设备状态
"query_flight_history", # 查询历史轨迹
"query_battery", # 查询电量
"query_statistics", # 统计报告
"unknown" # 无法识别
] = Field(description="识别出的用户意图")
drone_id: Optional[str] = Field(None, description="提取的无人机ID")
time_range: Optional[str] = Field(None, description="提取的时间范围")
class IntentRouter:
"""意图路由器 ------ LLM 分类 + 规则兜底
注意:LangGraph 的 Tool Calling 模式是主流程,意图路由仅作为可选增强层。
当 Tool Calling 准确率足够高时,可以跳过意图路由直接进入工具调用。
"""
def __init__(self, client: AsyncOpenAI, model: str = "deepseek-v3"):
self.client = client
self.model = model
async def route(self, user_input: str) -> IntentClassification:
response = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": """你是一个业务数据查询助手(以无人机场景为例)。根据用户的问题,识别意图并提取关键参数。
意图说明(以无人机场景为例):
- query_position: 查询无人机实时位置、经纬度、高度
- query_status: 查询无人机整体状态、在线/离线
- query_flight_history: 查询历史飞行轨迹、航线
- query_battery: 查询电量、续航时间
- query_statistics: 查询统计报告、飞行时长、里程
- unknown: 无法归类到以上任一意图"""},
{"role": "user", "content": user_input}
],
response_format={"type": "json_object"},
temperature=0.1
)
return IntentClassification.model_validate_json(
response.choices[0].message.content
)
4.4 语义工具适配器
这是隔离 Apifox API 变动对智能体影响的关键层。每个工具通过 Pydantic 定义严格的输入输出 Schema,内部直接通过 HTTP 调用 Apifox 业务接口。
python
# src/tools/adapters.py
import json
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field
import httpx
class PositionInput(BaseModel):
drone_id: str = Field(description="无人机唯一标识符,如 DJI-7890")
class PositionOutput(BaseModel):
drone_id: str
latitude: float
longitude: float
altitude: float
speed: float
unit: str = "m/s"
async def query_drone_position(drone_id: str,
redis_client=None) -> str:
"""
查询无人机的实时位置信息,包括经纬度、高度、速度。
当用户询问无人机在哪里、飞到哪了时调用此工具。
"""
# 🔑 短期缓存:同一无人机 10 秒内不重复请求
cache_key = f"cache:position:{drone_id}"
if redis_client:
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.drone-service.com/v1/drones/{drone_id}/position",
headers={"Authorization": f"Bearer {APIFOX_API_KEY}"},
timeout=10.0
)
resp.raise_for_status()
data = resp.json()
result = {
"drone_id": drone_id,
"latitude": data.get("lat", 0.0),
"longitude": data.get("lng", 0.0),
"altitude": data.get("altitude", 0.0),
"speed": data.get("speed", 0.0),
"unit": "m/s"
}
# 写入缓存
if redis_client:
await redis_client.setex(cache_key, 10, json.dumps(result))
return result
async def query_battery_status(drone_id: str) -> Dict[str, Any]:
"""
查询无人机电池状态,包括剩余电量和预估续航时间。
当用户询问电量、还能飞多久时调用此工具。
"""
async with httpx.AsyncClient() as client:
resp = await client.get(
f"https://api.drone-service.com/v1/drones/{drone_id}/battery",
headers={"Authorization": f"Bearer {APIFOX_API_KEY}"}
)
data = resp.json()
return {
"drone_id": drone_id,
"battery_percent": data.get("battery", 0),
"remaining_time": data.get("remaining_minutes", 0)
}
设计说明 :工具函数为普通异步函数,不再依赖 LangChain 的
@tool装饰器。LangGraph 的 ToolNode 支持直接传入 Python 函数,自动根据函数签名和 docstring 生成工具 Schema。
4.5 状态更新器
每次工具调用返回后,自动提取关键实体并更新 L2 层,保持状态新鲜。
python
# src/core/state_updater.py
from typing import Any, Dict
from datetime import datetime
class StateUpdater:
"""自动提取并更新实体状态"""
def __init__(self, entity_manager):
self.entity_manager = entity_manager
async def update(self, session_id: str, user_id: str,
tool_name: str, tool_result: Dict[str, Any]):
updates = {}
# 从工具结果中提取实体并追踪
if isinstance(tool_result, dict):
if "drone_id" in tool_result:
# 追踪无人机实体
await self.entity_manager.track_entity(
session_id, user_id, "drone", tool_result["drone_id"]
)
if "battery_percent" in tool_result:
updates["last_battery"] = tool_result["battery_percent"]
# 记录最后操作
if tool_name:
updates["last_action"] = tool_name
updates["last_action_time"] = datetime.utcnow().isoformat()
if updates:
await self.entity_manager.update(session_id, user_id, updates)
五、Apifox API 集成方案
5.1 集成架构
采用直接 HTTP 调用方式,不依赖任何中间件或 MCP 协议。
#mermaid-svg-rrv40O2TCtNpJ1cM{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-rrv40O2TCtNpJ1cM .error-icon{fill:#552222;}#mermaid-svg-rrv40O2TCtNpJ1cM .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-rrv40O2TCtNpJ1cM .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-rrv40O2TCtNpJ1cM .marker{fill:#333333;stroke:#333333;}#mermaid-svg-rrv40O2TCtNpJ1cM .marker.cross{stroke:#333333;}#mermaid-svg-rrv40O2TCtNpJ1cM svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-rrv40O2TCtNpJ1cM p{margin:0;}#mermaid-svg-rrv40O2TCtNpJ1cM .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster-label text{fill:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster-label span{color:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster-label span p{background-color:transparent;}#mermaid-svg-rrv40O2TCtNpJ1cM .label text,#mermaid-svg-rrv40O2TCtNpJ1cM span{fill:#333;color:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM .node rect,#mermaid-svg-rrv40O2TCtNpJ1cM .node circle,#mermaid-svg-rrv40O2TCtNpJ1cM .node ellipse,#mermaid-svg-rrv40O2TCtNpJ1cM .node polygon,#mermaid-svg-rrv40O2TCtNpJ1cM .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-rrv40O2TCtNpJ1cM .rough-node .label text,#mermaid-svg-rrv40O2TCtNpJ1cM .node .label text,#mermaid-svg-rrv40O2TCtNpJ1cM .image-shape .label,#mermaid-svg-rrv40O2TCtNpJ1cM .icon-shape .label{text-anchor:middle;}#mermaid-svg-rrv40O2TCtNpJ1cM .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-rrv40O2TCtNpJ1cM .rough-node .label,#mermaid-svg-rrv40O2TCtNpJ1cM .node .label,#mermaid-svg-rrv40O2TCtNpJ1cM .image-shape .label,#mermaid-svg-rrv40O2TCtNpJ1cM .icon-shape .label{text-align:center;}#mermaid-svg-rrv40O2TCtNpJ1cM .node.clickable{cursor:pointer;}#mermaid-svg-rrv40O2TCtNpJ1cM .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-rrv40O2TCtNpJ1cM .arrowheadPath{fill:#333333;}#mermaid-svg-rrv40O2TCtNpJ1cM .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-rrv40O2TCtNpJ1cM .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-rrv40O2TCtNpJ1cM .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rrv40O2TCtNpJ1cM .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-rrv40O2TCtNpJ1cM .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rrv40O2TCtNpJ1cM .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster text{fill:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM .cluster span{color:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-rrv40O2TCtNpJ1cM .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-rrv40O2TCtNpJ1cM rect.text{fill:none;stroke-width:0;}#mermaid-svg-rrv40O2TCtNpJ1cM .icon-shape,#mermaid-svg-rrv40O2TCtNpJ1cM .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-rrv40O2TCtNpJ1cM .icon-shape p,#mermaid-svg-rrv40O2TCtNpJ1cM .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-rrv40O2TCtNpJ1cM .icon-shape .label rect,#mermaid-svg-rrv40O2TCtNpJ1cM .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-rrv40O2TCtNpJ1cM .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-rrv40O2TCtNpJ1cM .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-rrv40O2TCtNpJ1cM :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 外部系统
智能体系统
HTTP / httpx
JSON 响应
格式化数据
LangGraph 智能体
语义工具层
手动封装
Apifox 业务 API
5.2 API 客户端封装
python
# src/tools/apifox_client.py
import httpx
from typing import Optional, Dict, Any
from tenacity import retry, stop_after_attempt, wait_exponential
class ApifoxClient:
"""Apifox API 客户端(直接 HTTP 调用)"""
def __init__(self, base_url: str, api_key: str, timeout: float = 30.0):
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.timeout = timeout
self._client: Optional[httpx.AsyncClient] = None
async def __aenter__(self):
self._client = httpx.AsyncClient(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.api_key}"},
timeout=self.timeout
)
return self
async def __aexit__(self, *args):
if self._client:
await self._client.aclose()
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
async def get(self, path: str, params: Optional[Dict] = None) -> Dict[str, Any]:
"""GET 请求,带自动重试"""
if not self._client:
raise RuntimeError("Client not initialized. Use 'async with' context.")
resp = await self._client.get(path, params=params)
resp.raise_for_status()
return resp.json()
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10))
async def post(self, path: str, json: Optional[Dict] = None) -> Dict[str, Any]:
"""POST 请求,带自动重试"""
if not self._client:
raise RuntimeError("Client not initialized. Use 'async with' context.")
resp = await self._client.post(path, json=json)
resp.raise_for_status()
return resp.json()
5.3 工具注册中心
python
# src/tools/registry.py
from typing import List, Dict, Callable, Any
class ToolRegistry:
"""动态工具注册表(无 LangChain 依赖)"""
def __init__(self):
self._tools: Dict[str, Callable] = {}
def register(self, func: Callable):
"""注册一个工具函数"""
self._tools[func.__name__] = func
def get(self, name: str) -> Callable:
"""按名称获取工具"""
return self._tools.get(name)
def get_all(self) -> List[Callable]:
"""获取所有已注册工具"""
return list(self._tools.values())
def get_names(self) -> List[str]:
"""获取所有工具名称"""
return list(self._tools.keys())
# 初始化并注册所有工具
registry = ToolRegistry()
registry.register(query_drone_position)
registry.register(query_battery_status)
# ... 注册更多工具
六、Higress API 管理平台
6.1 为什么引入 Higress
存量 Apifox 业务接口数量大、分散管理,直接在应用代码中硬编码数十个 API 端点存在以下问题:
| 痛点 | 现状 | Higress 解决方案 |
|---|---|---|
| 端点分散 | 每个工具函数各自写死 API URL | 统一路由表,按 service/action 规则转发 |
| 限流不统一 | 每个 API 的调用频率限制难以全局管控 | Higress 网关层统一配置限流策略 |
| 版本管理 | API 升级需要改代码重新部署 | Higress 按 path/header 做灰度路由(v1→v2) |
| 鉴权分散 | Apifox API Key 散落在各工具函数中 | Higress 统一注入认证头,密钥集中管理 |
| 可观测性缺失 | 每次 HTTP 调用的延迟/成功率无统一视图 | Higress 内置 metrics,接入 Prometheus |
6.2 Higress 双角色架构
#mermaid-svg-uXfnXSg5lpUcYji5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uXfnXSg5lpUcYji5 .error-icon{fill:#552222;}#mermaid-svg-uXfnXSg5lpUcYji5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uXfnXSg5lpUcYji5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uXfnXSg5lpUcYji5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uXfnXSg5lpUcYji5 .marker.cross{stroke:#333333;}#mermaid-svg-uXfnXSg5lpUcYji5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uXfnXSg5lpUcYji5 p{margin:0;}#mermaid-svg-uXfnXSg5lpUcYji5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster-label text{fill:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster-label span{color:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster-label span p{background-color:transparent;}#mermaid-svg-uXfnXSg5lpUcYji5 .label text,#mermaid-svg-uXfnXSg5lpUcYji5 span{fill:#333;color:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 .node rect,#mermaid-svg-uXfnXSg5lpUcYji5 .node circle,#mermaid-svg-uXfnXSg5lpUcYji5 .node ellipse,#mermaid-svg-uXfnXSg5lpUcYji5 .node polygon,#mermaid-svg-uXfnXSg5lpUcYji5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uXfnXSg5lpUcYji5 .rough-node .label text,#mermaid-svg-uXfnXSg5lpUcYji5 .node .label text,#mermaid-svg-uXfnXSg5lpUcYji5 .image-shape .label,#mermaid-svg-uXfnXSg5lpUcYji5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-uXfnXSg5lpUcYji5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uXfnXSg5lpUcYji5 .rough-node .label,#mermaid-svg-uXfnXSg5lpUcYji5 .node .label,#mermaid-svg-uXfnXSg5lpUcYji5 .image-shape .label,#mermaid-svg-uXfnXSg5lpUcYji5 .icon-shape .label{text-align:center;}#mermaid-svg-uXfnXSg5lpUcYji5 .node.clickable{cursor:pointer;}#mermaid-svg-uXfnXSg5lpUcYji5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uXfnXSg5lpUcYji5 .arrowheadPath{fill:#333333;}#mermaid-svg-uXfnXSg5lpUcYji5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uXfnXSg5lpUcYji5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uXfnXSg5lpUcYji5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uXfnXSg5lpUcYji5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uXfnXSg5lpUcYji5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uXfnXSg5lpUcYji5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster text{fill:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 .cluster span{color:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uXfnXSg5lpUcYji5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uXfnXSg5lpUcYji5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-uXfnXSg5lpUcYji5 .icon-shape,#mermaid-svg-uXfnXSg5lpUcYji5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uXfnXSg5lpUcYji5 .icon-shape p,#mermaid-svg-uXfnXSg5lpUcYji5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uXfnXSg5lpUcYji5 .icon-shape .label rect,#mermaid-svg-uXfnXSg5lpUcYji5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uXfnXSg5lpUcYji5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uXfnXSg5lpUcYji5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uXfnXSg5lpUcYji5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 内部服务
Higress
外部
HTTPS
路由 /chat/*
HTTP 调用
路由转发
用户客户端
Apifox 无人机 API
/v1/drones/*
/v1/flights/*
/v1/battery/*
...
入口网关 :7443
JWT鉴权 / 限流 / HTTPS
API管理平面 :8080
业务接口路由 / 版本 / 重试
FastAPI :8000
智能体服务
Redis
PostgreSQL
6.3 Higress 路由配置示例
入口路由(用户→智能体):
yaml
# higress/routes/entry.yaml
apiVersion: higress.io/v1
kind: McpBridge # Higress 的路由配置资源
metadata:
name: drone-agent-entry
spec:
rules:
# 聊天接口路由到 FastAPI
- match:
- path: /chat
methods: [POST]
backend:
service: fastapi-service.drone.svc.cluster.local
port: 8000
auth:
jwt:
secret: ${JWT_SECRET}
rateLimit:
requestsPerSecond: 10
# 健康检查
- match:
- path: /health
methods: [GET]
backend:
service: fastapi-service.drone.svc.cluster.local
port: 8000
API 管理路由(智能体→业务 API):
yaml
# higress/routes/backend.yaml
apiVersion: higress.io/v1
kind: McpBridge
metadata:
name: drone-api-management
spec:
rules:
# 位置查询
- match:
- path: /api/drones/{drone_id}/position
backend:
service: apifox-gateway.drone.svc.cluster.local
port: 443
protocol: HTTPS
rewrite:
uri: /v1/drones/{drone_id}/position
rateLimit:
requestsPerSecond: 50
retry:
attempts: 3
timeout: 10s
# 电池状态
- match:
- path: /api/drones/{drone_id}/battery
backend:
service: apifox-gateway.drone.svc.cluster.local
port: 443
protocol: HTTPS
rewrite:
uri: /v1/drones/{drone_id}/battery
# 飞行历史
- match:
- path: /api/drones/{drone_id}/history
backend:
service: apifox-gateway.drone.svc.cluster.local
port: 443
protocol: HTTPS
rewrite:
uri: /v1/drones/{drone_id}/flight-history
6.4 工具适配器简化
引入 Higress 后,工具函数不再需要硬编码完整 URL 和 API Key,改为调用 Higress 统一端点:
python
# src/tools/adapters.py(简化版)
HIGRESS_BASE = "http://higress-api-mgmt:8080/api"
async def query_drone_position(drone_id: str, redis_client=None) -> dict:
"""查询无人机实时位置"""
cache_key = f"cache:position:{drone_id}"
if redis_client:
cached = await redis_client.get(cache_key)
if cached:
return json.loads(cached)
async with httpx.AsyncClient(timeout=10.0) as client:
# 🔑 不再拼 Apifox URL,统一走 Higress 路由
resp = await client.get(
f"{HIGRESS_BASE}/drones/{drone_id}/position"
)
resp.raise_for_status()
data = resp.json()
result = { ... } # 数据清洗不变
if redis_client:
await redis_client.setex(cache_key, 10, json.dumps(result))
return result
关键收益 :工具函数只需知道
{HIGRESS_BASE}/drones/{id}/position,API 鉴权、限流、重试、版本切换全部由 Higress 网关层处理,代码零改动即可切换后端 API 版本。
6.5 简化应用层容错代码
引入 Higress 后,应用层可以精简部分容错逻辑:
| 能力 | 之前(应用层) | 之后(Higress) |
|---|---|---|
| 限流 | 自建 Rate Limiter | Higress 网关层统一限流 |
| 重试 | tenacity @retry 装饰器 |
Higress 路由配置 retry.attempts |
| 鉴权 | 每个工具函数硬编码 Bearer {KEY} |
Higress 统一注入认证头 |
| 熔断 | 自建 CircuitBreaker 类 | Higress 基于 Envoy 原生熔断 |
| 缓存 | 应用层 Redis 缓存 | 保留(业务语义,不可下沉) |
⚠️ 注意:API 结果缓存保留在应用层,因为缓存策略与业务语义强相关(如"位置缓存 10 秒"),不适合下沉到通用网关。
七、LangGraph 工作流设计
7.1 工作流图定义
使用 LangGraph 定义智能体的工作流:
python
# src/core/graph.py
from typing import TypedDict, Annotated, List
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode
from langgraph.checkpoint.redis import RedisSaver
class AgentState(TypedDict):
"""智能体状态"""
messages: Annotated[list, add_messages]
session_id: str
user_id: str
def create_agent_graph(tools: List, llm):
"""创建智能体工作流图
tools 为普通 Python 函数列表,LangGraph 的 ToolNode 自动处理 Schema 生成。
使用 bind_tools 将工具绑定到 LLM 模型。
"""
# 创建工具节点
tool_node = ToolNode(tools)
# 创建 LLM 节点(绑定工具)
model_with_tools = llm.bind_tools(tools)
async def call_model(state: AgentState):
"""调用 LLM 节点"""
response = await model_with_tools.ainvoke(state["messages"])
return {"messages": [response]}
def should_continue(state: AgentState) -> str:
"""判断是否继续执行工具调用"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "end"
# 构建图
workflow = StateGraph(AgentState)
# 添加节点
workflow.add_node("agent", call_model)
workflow.add_node("tools", tool_node)
# 添加边
workflow.set_entry_point("agent")
workflow.add_conditional_edges("agent", should_continue, {
"tools": "tools",
"end": END
})
workflow.add_edge("tools", "agent")
return workflow.compile()
关键变更 :去掉了
current_drone_id字段(由 L2 实体层管理),新增user_id字段用于 Redis Key 隔离。工具列表直接传入普通 Python 函数即可。
7.2 Checkpointer 配置(状态持久化)
python
# src/core/checkpointer.py
from langgraph.checkpoint.redis import RedisSaver
import redis.asyncio as redis
async def create_checkpointer(redis_url: str) -> RedisSaver:
"""创建 Redis Checkpointer"""
redis_client = redis.from_url(redis_url)
checkpointer = RedisSaver(redis_client)
await checkpointer.asetup()
return checkpointer
八、FastAPI 接口层
8.1 对话接口
python
# src/api/routes/chat.py
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import Optional
router = APIRouter(prefix="/chat", tags=["对话"])
class ChatRequest(BaseModel):
session_id: str
user_id: str
message: str
class ChatResponse(BaseModel):
session_id: str
response: str
tool_calls: Optional[list] = None
@router.post("/send", response_model=ChatResponse)
async def send_message(request: ChatRequest):
"""
发送对话消息,获取智能体回复
"""
try:
# 1. 预处理(指代消解)
processed_input = await reference_middleware.process(
request.message,
request.session_id,
request.user_id
)
# 2. 调用智能体
result = await agent.ainvoke({
"messages": [HumanMessage(content=processed_input)],
"session_id": request.session_id,
"user_id": request.user_id
})
# 3. 更新实体状态
last_message = result["messages"][-1]
return ChatResponse(
session_id=request.session_id,
response=last_message.content,
tool_calls=getattr(last_message, "tool_calls", None)
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
8.2 流式响应接口
python
@router.post("/stream")
async def stream_chat(request: ChatRequest):
"""流式对话响应"""
from fastapi.responses import StreamingResponse
import json
async def generate():
processed_input = await reference_middleware.process(
request.message,
request.session_id,
request.user_id
)
async for chunk in agent.astream({
"messages": [HumanMessage(content=processed_input)],
"session_id": request.session_id
}):
yield json.dumps(chunk) + "\n"
return StreamingResponse(generate(), media_type="text/plain")
九、容错与异常处理
9.1 异常分类与处理策略
| 异常类型 | 场景 | 处理策略 | 用户提示 |
|---|---|---|---|
| LLM 超时 | 模型响应超过 30s | 重试 1 次 → 降级为规则兜底 | "服务繁忙,请稍后再试" |
| LLM 返回格式错误 | JSON 解析失败 | 重试 1 次(降低 temperature)→ 默认意图 | "抱歉,我没理解您的问题,请换个说法" |
| Apifox API 超时 | 业务接口超过 10s | 重试 2 次 → 返回缓存数据 → 降级提示 | "数据获取超时,显示的是最近一次缓存数据" |
| Apifox API 返回异常 | 4xx/5xx 错误 | 记录日志 → 告知用户具体原因 | "查询失败:无人机 {id} 当前离线" |
| 工具调用死循环 | LLM 反复调用同一工具 | 限制最大工具调用次数(5 次)→ 强制终止 | "查询步骤过多,请简化您的问题" |
| Redis 不可用 | 连接断开 | 降级为无状态模式(仅当前轮对话) | 静默降级,不影响响应 |
9.2 工具调用循环防护
python
# src/core/graph.py 中增加循环检测
MAX_TOOL_CALLS = 5 # 最大工具调用次数
class AgentState(TypedDict):
messages: Annotated[list, add_messages]
session_id: str
user_id: str
tool_call_count: int # 新增:工具调用计数器
def should_continue(state: AgentState) -> str:
"""判断是否继续执行工具调用,增加循环防护"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
count = state.get("tool_call_count", 0) + 1
if count > MAX_TOOL_CALLS:
# 强制终止,避免无限循环
return "end"
state["tool_call_count"] = count
return "tools"
return "end"
9.3 熔断器模式
python
# src/tools/circuit_breaker.py
import asyncio
from datetime import datetime, timedelta
class CircuitBreaker:
"""API 调用熔断器"""
def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 30):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout # 秒
self.failure_count = 0
self.last_failure_time: Optional[datetime] = None
self.state = "CLOSED" # CLOSED / OPEN / HALF_OPEN
async def call(self, func, *args, **kwargs):
if self.state == "OPEN":
if datetime.utcnow() - self.last_failure_time > timedelta(seconds=self.recovery_timeout):
self.state = "HALF_OPEN"
else:
raise Exception("Circuit breaker is OPEN")
try:
result = await func(*args, **kwargs)
if self.state == "HALF_OPEN":
self.state = "CLOSED"
self.failure_count = 0
return result
except Exception:
self.failure_count += 1
self.last_failure_time = datetime.utcnow()
if self.failure_count >= self.failure_threshold:
self.state = "OPEN"
raise
9.4 全局异常中间件
python
# src/middleware/error_handler.py
from fastapi import Request
from fastapi.responses import JSONResponse
async def global_exception_handler(request: Request, exc: Exception):
"""全局异常处理,统一错误响应格式"""
logger.error("unhandled_error",
path=request.url.path,
error=str(exc),
error_type=type(exc).__name__
)
return JSONResponse(
status_code=500,
content={
"error": "internal_error",
"message": "服务内部错误,请稍后重试",
"session_id": getattr(request.state, "session_id", None)
}
)
十、Prompt 管理与对话恢复
10.1 Prompt 模板外置
Prompt 不应硬编码在代码中,统一存放在 config/prompts/ 目录下:
config/prompts/
├── system_prompt.yaml # 系统主 Prompt
├── intent_router.yaml # 意图路由 Prompt
├── tool_descriptions.yaml # 工具描述 Prompt
└── fallback.yaml # 降级兜底 Prompt
示例:config/prompts/system_prompt.yaml
yaml
system_prompt: |
你是一个专业的业务数据查询助手(以无人机场景为例)。你可以帮助用户查询以下信息:
- 设备实时位置(经纬度、高度、速度)
- 设备运行状态(在线/离线、任务状态)
- 历史轨迹记录
- 电量/资源余量
- 统计报告
规则:
1. 回答简洁准确,使用中文
2. 数值保留一位小数,附上单位
3. 如果数据获取失败,如实告知用户原因
4. 涉及多个实体时,分别列出各自数据
context_injection: |
[上下文信息]
当前焦点实体: {focus_entity_id}
最近活跃实体: {active_entities}
python
# src/core/prompt_loader.py
import yaml
from pathlib import Path
from string import Template
class PromptManager:
"""Prompt 管理器 ------ 支持模板加载和变量注入"""
def __init__(self, prompts_dir: str = "config/prompts"):
self.prompts_dir = Path(prompts_dir)
self._cache: dict = {}
def load(self, name: str) -> str:
"""加载指定 Prompt,支持缓存"""
if name not in self._cache:
filepath = self.prompts_dir / f"{name}.yaml"
with open(filepath, "r", encoding="utf-8") as f:
self._cache[name] = yaml.safe_load(f)
return self._cache[name]
def render(self, name: str, **variables) -> str:
"""加载并渲染 Prompt,注入变量"""
template = self.load(name)
prompt = template.get("system_prompt", "")
return Template(prompt).safe_substitute(**variables)
10.2 对话中断恢复机制
LangGraph 的 Checkpointer 支持保存工作流中间状态,实现对话中断后无缝恢复:
python
# src/core/checkpointer.py
from langgraph.checkpoint.redis import RedisSaver
import redis.asyncio as redis
async def create_checkpointer(redis_url: str) -> RedisSaver:
"""创建 Redis Checkpointer,支持会话中断恢复"""
redis_client = redis.from_url(redis_url)
checkpointer = RedisSaver(redis_client)
await checkpointer.asetup()
return checkpointer
# 恢复对话示例
async def resume_conversation(session_id: str, user_id: str, new_message: str):
"""从 Checkpointer 恢复历史会话状态"""
config = {"configurable": {"thread_id": session_id}}
# 获取历史状态
past_state = await graph.aget_state(config)
if past_state.values:
# 在历史状态基础上继续
result = await graph.ainvoke(
{"messages": [HumanMessage(content=new_message)]},
config
)
else:
# 新会话
result = await graph.ainvoke({
"messages": [HumanMessage(content=new_message)],
"session_id": session_id,
"user_id": user_id
}, config)
return result
注意:Checkpointer 保存的是 LangGraph 工作流的完整状态快照,Redis TTL 失效后会话将不可恢复,需配合 L1/L2 的 TTL 设置一致。
十一、部署方案
11.1 部署架构
#mermaid-svg-UZwGqOoaaN1PR1rO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UZwGqOoaaN1PR1rO .error-icon{fill:#552222;}#mermaid-svg-UZwGqOoaaN1PR1rO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UZwGqOoaaN1PR1rO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UZwGqOoaaN1PR1rO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UZwGqOoaaN1PR1rO .marker.cross{stroke:#333333;}#mermaid-svg-UZwGqOoaaN1PR1rO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UZwGqOoaaN1PR1rO p{margin:0;}#mermaid-svg-UZwGqOoaaN1PR1rO .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster-label text{fill:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster-label span{color:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster-label span p{background-color:transparent;}#mermaid-svg-UZwGqOoaaN1PR1rO .label text,#mermaid-svg-UZwGqOoaaN1PR1rO span{fill:#333;color:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO .node rect,#mermaid-svg-UZwGqOoaaN1PR1rO .node circle,#mermaid-svg-UZwGqOoaaN1PR1rO .node ellipse,#mermaid-svg-UZwGqOoaaN1PR1rO .node polygon,#mermaid-svg-UZwGqOoaaN1PR1rO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UZwGqOoaaN1PR1rO .rough-node .label text,#mermaid-svg-UZwGqOoaaN1PR1rO .node .label text,#mermaid-svg-UZwGqOoaaN1PR1rO .image-shape .label,#mermaid-svg-UZwGqOoaaN1PR1rO .icon-shape .label{text-anchor:middle;}#mermaid-svg-UZwGqOoaaN1PR1rO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UZwGqOoaaN1PR1rO .rough-node .label,#mermaid-svg-UZwGqOoaaN1PR1rO .node .label,#mermaid-svg-UZwGqOoaaN1PR1rO .image-shape .label,#mermaid-svg-UZwGqOoaaN1PR1rO .icon-shape .label{text-align:center;}#mermaid-svg-UZwGqOoaaN1PR1rO .node.clickable{cursor:pointer;}#mermaid-svg-UZwGqOoaaN1PR1rO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UZwGqOoaaN1PR1rO .arrowheadPath{fill:#333333;}#mermaid-svg-UZwGqOoaaN1PR1rO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UZwGqOoaaN1PR1rO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UZwGqOoaaN1PR1rO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UZwGqOoaaN1PR1rO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UZwGqOoaaN1PR1rO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UZwGqOoaaN1PR1rO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster text{fill:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO .cluster span{color:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UZwGqOoaaN1PR1rO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UZwGqOoaaN1PR1rO rect.text{fill:none;stroke-width:0;}#mermaid-svg-UZwGqOoaaN1PR1rO .icon-shape,#mermaid-svg-UZwGqOoaaN1PR1rO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UZwGqOoaaN1PR1rO .icon-shape p,#mermaid-svg-UZwGqOoaaN1PR1rO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UZwGqOoaaN1PR1rO .icon-shape .label rect,#mermaid-svg-UZwGqOoaaN1PR1rO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UZwGqOoaaN1PR1rO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UZwGqOoaaN1PR1rO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UZwGqOoaaN1PR1rO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 控制平面
HTTPS
会话状态/检查点
长期记忆
追踪日志
HTTP 调用
路由转发
网关指标
访问日志
LLM API
用户
Higress 入口网关
:7443
FastAPI 服务
副本数: 2-4
Redis
主从集群
PostgreSQL + pgvector
LangFuse
Higress API 管理平面
:8080
Apifox 业务 API
Prometheus
Loki
DeepSeek / Qwen
11.2 Docker Compose 配置
yaml
# docker/docker-compose.yml
version: '3.8'
services:
# ===== Higress API 网关 =====
higress:
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/higress:latest
ports:
- "7443:7443" # 入口网关(HTTPS)
- "8080:8080" # API 管理平面
environment:
- JWT_SECRET=${JWT_SECRET}
- APIFOX_API_KEY=${APIFOX_API_KEY}
volumes:
- ./higress/routes:/etc/higress/routes
depends_on:
- api
- redis
restart: unless-stopped
api:
build:
context: ..
dockerfile: docker/Dockerfile
ports:
- "8000:8000"
environment:
- REDIS_URL=redis://redis:6379/0
- DATABASE_URL=postgresql://postgres:postgres@postgres:5432/drone
- HIGRESS_MGMT_URL=http://higress:8080/api # API 管理平面地址
- OPENAI_API_KEY=${OPENAI_API_KEY}
- OPENAI_BASE_URL=${OPENAI_BASE_URL}
depends_on:
- redis
- postgres
command: uvicorn src.main:app --host 0.0.0.0 --port 8000 --workers 4
restart: unless-stopped
redis:
image: redis:7.2-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --save 60 1 --loglevel warning
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16 # 内置 pgvector 扩展
environment:
- POSTGRES_DB=drone
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
volumes:
redis_data:
postgres_data:
11.3 环境变量配置
bash
# .env.production
# ===== LLM 配置 =====
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://api.atlascloud.ai/v1
DEFAULT_MODEL=deepseek-ai/deepseek-v3
LLM_TEMPERATURE=0.1
MAX_TOKENS=4096
# ===== Redis =====
REDIS_URL=redis://redis:6379/0
REDIS_SESSION_TTL=1800 # L1: 30分钟
REDIS_ENTITY_TTL=7200 # L2: 2小时
# ===== PostgreSQL =====
DATABASE_URL=postgresql://postgres:postgres@postgres:5432/drone
DB_POOL_SIZE=10
DB_MAX_OVERFLOW=20
# ===== Apifox API(经 Higress 路由) =====
HIGRESS_MGMT_URL=http://higress:8080/api
APIFOX_API_KEY=xxx # 由 Higress 统一注入,应用层无需感知
# ===== 可观测性 =====
LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=xxx
LANGCHAIN_PROJECT=drone-agent
# ===== 日志 =====
LOG_LEVEL=INFO
LOG_FORMAT=json
十二、可观测性设计
12.1 三大监控支柱
| 支柱 | 工具 | 监控内容 |
|---|---|---|
| 链路追踪 | LangSmith | 每次 LLM 调用的输入输出、Token 消耗、延迟 |
| 指标监控 | Prometheus + Grafana | 请求 QPS、错误率、P99 延迟、Token 成本 |
| 日志聚合 | ELK / Loki | 全链路结构化日志,支持按 session_id/trace_id 检索 |
12.2 关键指标
| 类别 | 指标 | 目标值 |
|---|---|---|
| 业务 | 意图识别准确率 | ≥ 90% |
| 业务 | 工具调用成功率 | ≥ 95% |
| 技术 | API P95 响应时间 | ≤ 3s |
| 技术 | LLM 调用延迟 | ≤ 2s |
| 成本 | 单次对话平均成本 | ≤ ¥0.10 |
| 可用性 | 服务可用性 | ≥ 99.9% |
12.3 日志规范
python
# src/utils/logging.py
import structlog
logger = structlog.get_logger()
# 使用示例
logger.info("agent_call",
session_id=session_id,
user_id=user_id,
intent=result.intent,
tool_calls=result.tool_calls,
token_usage=result.token_usage,
latency_ms=elapsed_ms
)
十三、扩展性设计
当前架构已预留三个扩展接口,为未来引入更复杂的智能体能力铺路。
13.1 规划器接口(Planner)
当前使用直接路由,未来可切换为 Plan-and-Execute 模式:
python
# 预留接口
class Planner:
async def plan(self, query: str, context: dict) -> List[PlanStep]:
"""将复杂问题分解为多步计划"""
pass
# 未来切换
# agent = create_plan_execute_agent(planner=Planner(), executor=executor)
13.2 工具注册中心(Tool Registry)
已实现动态挂载新工具的能力。未来接入 OCR 等新能力时,只需:
python
# 注册新工具
from src.tools.registry import registry
from src.tools.ocr_tool import ocr_batch_process
registry.register(ocr_batch_process) # 一行代码扩展
13.3 多智能体协作接口
未来可引入监督者模式(Supervisor Pattern):
python
# 预留的多智能体架构
supervisor = create_supervisor_agent(
agents={
"data_expert": data_query_agent, # 数据查询专家
"report_writer": report_agent, # 报告撰写专家
"image_analyzer": ocr_agent # 图片分析专家
}
)
十四、风险与应对
| 风险 | 影响 | 概率 | 应对措施 |
|---|---|---|---|
| LLM 调用成本失控 | 高 | 高 | 三层记忆设计减少 Token 消耗;设置单次对话 Token 上限(4000);工具调用最大次数限制(5次);使用性价比模型(DeepSeek) |
| 工具调用准确率低 | 中 | 中 | 精选 5-8 个核心工具;优化工具 docstring 描述;意图路由作为规则兜底 |
| 工具调用死循环 | 中 | 中 | 最大调用次数限制(5次)强制终止;监控告警异常循环 |
| Apifox API 不稳定 | 中 | 低 | 增加重试机制(3次);熔断器保护;结果短期缓存(10秒) |
| 会话状态泄露 | 高 | 中 | Redis Key 包含 user_id 隔离(user:{uid}:session:{sid}:entity);敏感数据脱敏;设置 TTL 自动清理 |
| 多轮对话上下文混淆 | 中 | 中 | L2 多实体追踪 + 预处理中间件指代消解;支持 focus_entity 精确消歧 |
| 指代消解覆盖不足 | 中 | 中 | 字符串匹配覆盖常见指代词(70% 场景);复杂指代预留 LLM 辅助消解接口 |
| Apifox API 字段变更 | 低 | 中 | 工具适配器隔离 Schema 变更;增加 API 响应校验(Pydantic 验证) |
| Higress 网关单点故障 | 高 | 低 | 部署多副本 + Kubernetes 自动故障转移;降级方案:工具适配器保留直连 Apifox 的 fallback 能力 |
| LangGraph 版本升级 | 低 | 中 | 锁定版本号;定期评估升级影响;减少对 LangChain 的依赖降低升级风险 |
十五、项目里程碑
| 阶段 | 周期 | 关键交付物 | 验收标准 |
|---|---|---|---|
| P0:环境搭建 | 第 1 周 | 开发环境、CI/CD 流水线、LangGraph 基础框架搭建 | 服务可本地启动,健康检查通过 |
| P1:核心智能体 | 第 2-3 周 | Tool Calling 智能体 + 5 个核心工具 + Redis L1/L2 记忆 | 单轮对话准确率 ≥ 85%,多轮指代消解 ≥ 90% |
| P2:容错与优化 | 第 4 周 | 熔断器、循环防护、Prompt 外置管理、对话恢复 | P95 延迟 < 3s,异常场景优雅降级 |
| P3:全面测试 | 第 5 周 | 压测报告、Prompt 调优、异常场景覆盖 | 工具调用成功率 ≥ 95%,成本可控 |
| P4:生产上线 | 第 6 周 | 生产部署、监控告警 | 灰度验证通过,正式发布 |
| P5:长期记忆 | 第 7-8 周 | PostgreSQL L3a 结构化配置 + pgvector L3b 语义记忆 | 跨会话用户偏好生效,历史对话语义检索可用 |
| P6:高级能力 | 长期 | OCR 接入、多智能体协作、Plan-Execute 模式 | 按需迭代 |
十六、附录
A. 完整依赖清单
toml
# pyproject.toml
[project]
name = "drone-agent-system"
version = "1.0.0"
description = "无人机数据对话查询智能体"
authors = [{name = "AI Engineering Team"}]
dependencies = [
"fastapi>=0.115.0",
"uvicorn[standard]>=0.30.0",
"langgraph>=0.2.0",
"langgraph-checkpoint-redis>=0.1.0",
"langsmith>=0.1.0",
"redis>=5.0.0",
"httpx>=0.27.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
"openai>=1.0.0",
"tenacity>=8.0.0",
"structlog>=24.0.0",
"python-dotenv>=1.0.0",
"pyyaml>=6.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-asyncio>=0.23.0",
"black>=24.0.0",
"ruff>=0.4.0",
"mypy>=1.8.0",
]
# 阶段二启用
phase2 = [
"asyncpg>=0.30.0",
"sqlalchemy>=2.0.0",
"pgvector>=0.3.0", # PostgreSQL 向量扩展
"openai>=1.0.0", # Embedding API
]
B. 目录结构
drone-agent-system/
├── src/
│ ├── api/
│ │ ├── routes/
│ │ │ ├── chat.py # 对话接口
│ │ │ └── health.py # 健康检查
│ │ └── dependencies.py # 依赖注入
│ ├── core/
│ │ ├── agent.py # LangGraph 智能体
│ │ ├── graph.py # 工作流图编排(含循环防护)
│ │ ├── checkpointer.py # Redis Checkpointer(对话恢复)
│ │ ├── state_updater.py # 状态更新器(多实体追踪)
│ │ ├── router.py # 意图路由器(可选)
│ │ └── prompt_loader.py # Prompt 管理器
│ ├── memory/
│ │ ├── l1_buffer.py # L1 对话缓冲
│ │ ├── l2_entity.py # L2 多实体状态管理
│ │ ├── l3a_preferences.py # L3a 结构化配置(阶段二)
│ │ └── l3b_semantic.py # L3b 语义记忆(阶段二)
│ ├── tools/
│ │ ├── adapters.py # 语义工具定义(含缓存)
│ │ ├── apifox_client.py # Apifox HTTP 客户端
│ │ ├── registry.py # 工具注册中心
│ │ └── circuit_breaker.py # 熔断器
│ ├── middleware/
│ │ ├── reference.py # 指代消解
│ │ ├── error_handler.py # 全局异常处理
│ │ └── logging.py # 日志中间件
│ ├── models/
│ │ └── schemas.py # Pydantic 数据模型
│ └── main.py # FastAPI 入口
├── config/
│ ├── settings.py # 配置管理
│ └── prompts/ # Prompt 模板(外置)
│ ├── system_prompt.yaml
│ ├── intent_router.yaml
│ └── tool_descriptions.yaml
├── tests/
│ ├── unit/
│ └── integration/
├── docker/
│ ├── Dockerfile
│ └── docker-compose.yml
├── higress/
│ └── routes/
│ ├── entry.yaml # 入口路由配置
│ └── backend.yaml # 业务 API 路由配置
├── scripts/
│ └── seed_db.py # 初始化数据
├── pyproject.toml
├── .env.example
└── README.md
C. 术语表
| 术语 | 说明 |
|---|---|
| LangGraph | LangChain 生态的有状态多步骤工作流编排框架 |
| LCEL | LangChain Expression Language,LangChain 的链式调用语法 |
| Checkpointer | LangGraph 的状态持久化机制,支持会话恢复 |
| 指代消解 | 识别并解析"它"、"那架"等代词指向的具体实体 |
| 语义工具 | 对原始 API 进行封装,包含清晰的输入输出 Schema 和自然语言描述 |
| 工具调用 | LLM 根据用户意图选择并调用合适工具的能力 |