Files
xk-ai-agent/docs/architecture.md
2026-08-14 21:50:48 +08:00

224 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TCM Agent 架构说明(现状版)
> 更新时间2026-08-11
> 适用版本:加固 + 观测面板落地之后的代码(含 RunLog / 医疗守卫 / DB 配置热加载 / Function Calling 完整协议)
本文档回答四个问题:**现在长什么样(架构)、一次请求怎么走(流程)、哪里还有坑(隐患)、为什么这样设计(优势)**。
---
## 一、架构总览
### 1.1 系统定位
Go Agent 是「PHP 主业务系统」的 **AI 增强侧车**PHP 负责组装 prompt、落库、面向前端Go 负责知识库检索、ReAct 多步推理、多模型调度与降级。PHP 挂了 Go 没意义Go 挂了 PHP 可以降级为直连模型(`ai_agent_via_agent=0`)。
```mermaid
flowchart TB
subgraph php[PHP 主系统 xk-api]
assist[AiMedicalAssistService<br/>组装 system/user messages]
factory[AiAgentFactory<br/>按 ai_agent_via_agent 决定直连或走 Go]
stepTable[(xk_ai_generation_step<br/>历史步骤审计表)]
end
subgraph go[Go Agent nl-tcm-agent]
auth[middleware.Auth<br/>JWT + SharedSecret 双轨]
sem[并发信号量 16<br/>超限 503]
enhancer[EnhancerService.Enhance<br/>包装层:统一埋点]
guard[medical_guard<br/>医疗相关性守卫]
react[ReactLoop<br/>Plan → Think-Act-Observe → Reflection]
resolve[resolveClient<br/>DB 优先解析生效模型]
runlog[RunLog 环形缓冲<br/>内存 200 条]
panel[/agent/view 观测面板/]
end
subgraph db[MySQL z_xk]
syscfg[(xk_system_config<br/>生效模型/开关)]
aikey[(xk_ai_model / xk_ai_api_key<br/>模型池与密钥 AES 加密)]
kbtbl[(xk_kb_* 本地知识库)]
end
subgraph llmv[模型供应商]
spark[讯飞 Spark]
dsk[DeepSeek]
oai[OpenAI 兼容]
end
assist --> factory -->|POST /api/v1/agent/enhance| auth --> sem --> enhancer
enhancer --> guard --> react
enhancer --> resolve --> syscfg
resolve --> aikey
react --> spark & dsk & oai
react -->|kb_enabled| kbtbl
enhancer -->|每次运行埋点| runlog --> panel
enhancer -->|steps 随响应返回| assist --> stepTable
```
### 1.2 目录与模块职责
| 目录 | 职责 | 关键点 |
|---|---|---|
| `main.go` | 启动入口 | config → dao → llm.InitLLM → agent.InitRunner → router.Setup`http.Server` 带四组超时;优雅停机 |
| `internal/config` | YAML/env 静态配置 | 只做启动期兜底;运行期以 DB 为准 |
| `internal/dao` | 数据库访问 | `LoadActiveLLMConfig`60s TTL 缓存)从 `xk_system_config` 实时解析生效模型AES 解密密钥 |
| `internal/agentcfg` | Agent 行为配置 | 从 `xk_system_config` 读 ReAct 开关、医疗守卫开关、debug 日志开关等 |
| `internal/llm` | 模型接入层 | `ProviderFactory` 创建客户端;`ModelRouter` 按场景路由 + `GetByConfig` 按配置指纹缓存/重建;`FallbackChain` 降级链 |
| `internal/service` | 业务核心 | `enhancer.go` 主流程、`reactloop.go` ReAct 循环、`medical_guard.go` 医疗守卫、`runlog.go` 运行轨迹缓冲 |
| `internal/agent` | 会话式 Runner | `/agent/chat` 用的多轮会话引擎 + TokenBudget与 EnhancerService 相互独立) |
| `internal/kb` | 本地知识库 | V1 关键词检索NoopEmbedder读写 `xk_kb_*`V2 预留向量化接口 |
| `internal/tool` | Function Calling 工具集 | MaxKB 检索等,注入 ReactLoop 供模型调用 |
| `internal/middleware` | 全局中间件 | 日志 / CORS / 双轨鉴权JWT + SharedSecret |
| `internal/router` | 路由注册 | 业务 API + 运维 API + 两个静态面板(`/kb/view``/agent/view` |
| `internal/security/xkaes` | AES 加解密 | 与 PHP `EncryptorService` 同算法,解密 DB 里的 API Key |
| `view/` | 前端单页 | `index.html`(知识库后台)、`agent.html`运行观测面板Vue3 + Element Plus CDN |
### 1.3 配置体系三层DB 为王)
```
优先级xk_system_configDB运行期实时 > 环境变量 > manifest/config/config.yaml启动期兜底
```
- **生效模型**`ai_active_provider` / `ai_active_model` / `ai_active_api_key_id` 三个键决定当前用哪个模型。Go 端 `dao.LoadActiveLLMConfig` 带 60s TTL 缓存,后台切换最多 1 分钟内全集群生效PHP 后台保存后还可调 `POST /api/v1/models/invalidate-cache` 立即生效。
- **走不走 Go Agent**:独立开关 `ai_agent_via_agent`PHP `AiAgentFactory` 读取),与「用哪个模型」彻底解耦——这是踩过坑后的关键设计:早期把 `ai_active_provider=agent` 当路由标记,导致模型配置和链路选择互相污染。
- **Agent 行为**`ai_agent_medical_guard`(守卫开关)、`ai_agent_debug_log`(请求体日志开关,含 PHI 默认关、ReAct 迭代数等,由 `agentcfg` 统一加载。
- **客户端缓存与指纹**`ModelRouter.GetByConfig` 对「model+url+key+provider」做指纹配置变了自动重建 LLM 客户端,没变就复用连接池。
---
## 二、一次 enhance 请求的完整流程
以 PHP 发起「开方建议」为例:
```mermaid
sequenceDiagram
participant PHP as PHP TcmAgentClient
participant MW as Auth + 信号量(16)
participant EN as EnhancerService.Enhance
participant GD as 医疗守卫
participant RC as resolveClient
participant RL as ReactLoop
participant LLM as LLM 供应商
participant LOG as RunLog 环形缓冲
PHP->>MW: POST /agent/enhance {scene, messages, kb_enabled}
MW->>MW: Bearer == SharedSecret并发 <16
MW->>EN: 通过(否则 401 / 503 快速失败)
EN->>GD: 白名单/黑名单关键词校验
alt 非医疗内容
GD-->>EN: 拦截
EN->>LOG: 记录 status=3守卫拦截
EN-->>PHP: 500 + medical_guard step
end
EN->>RC: 解析生效模型DB 60s 缓存 → yaml 兜底)
RC-->>EN: client + provider + api_key_id
EN->>RL: 进入 ReAct 循环
RL->>LLM: [1] Planning输出计划计划跑题则丢弃
loop Think-Act-Observe预算内多轮
RL->>LLM: [2] 带 tools 调用Lite 自动降级纯文本)
alt 触发 Function Calling
RL->>RL: 执行工具tool 结果带 tool_call_id 入栈
end
end
RL->>LLM: [3] Reflection 低温自检
RL->>LLM: [4] JSON 不合法时自动修复
RL-->>EN: content + steps[](每步耗时/token/detail
EN->>LOG: 记录 status=1/2 + 完整 steps
EN-->>PHP: {content, provider, model, steps, total_ms}
PHP->>PHP: steps 落 xk_ai_generation_step
```
关键细节:
1. **守卫在最前面**:不花任何 token 就能拒掉「PHP 组装出错 / 接口被滥用」的非医疗请求,同时留下 `medical_guard` 步骤便于定位是谁的问题。
2. **每次调用都实时解析模型**:不是启动时定死,后台切模型不用重启 Go。
3. **消息序列保证以 user/tool 结尾**Planning 注入后补 user 帧,兼容 Spark Lite 等严格遵循 OpenAI 协议的模型(曾因 assistant 结尾触发 10003
4. **网络错误自动重试 1 次**(间隔 1s只重试连接/超时类错误「API 返回 xxx」业务错误不重试避免双倍烧 token。
5. **无论成败都写 RunLog**包装层统一埋点守卫拦截、配置解析失败这类「PHP 侧看不到」的失败也有记录。
### 观测面板(/agent/view
- **数据链路**RunLog内存 200 条)→ `GET /api/v1/agent/runs`(列表摘要)/ `runs/:id`(完整时间线)/ `stats`成功率、平均耗时、token、按场景分布
- **三个 Tab**:运行记录(点行弹时间线抽屉)、统计概览、生效配置(复用 `/models/active-config` + `/health`)。
- **localStorage 记忆**:当前 Tab`agent_view_tab`)、鉴权 token`agent_view_token`)、自动刷新开关(`agent_view_refresh`5s 轮询)、主题与 KB 页共用 `kb_theme`
- 页面本身放行不鉴权(纯静态无数据),数据 API 走全局 Auth。
---
## 三、隐患清单(按风险排序)
### 高:模型能力短板不是代码能完全兜住的
- **Lite 模型「计划腔」污染输出**:即使内容是医疗的,输出仍可能带「执行阶段/步骤一」的项目管理腔调(守卫只拦非医疗关键词,拦不住文风跑偏)。实测已复现。
- **Reflection 审核不通过时内容照样返回**:目前 reflection 结果只作为附注拼进输出,没有「不合格 → 重试或换模型」的闭环。临床场景下这是最值得补的一环。
- 缓解方向:审核不通过时触发一次重生成或 fallback 到更强模型;或在 PHP 侧按 reflection 结果决定是否展示。
### 高:医疗守卫是关键词规则引擎
- 白名单/黑名单靠人工维护,存在**误杀**(新病种词不在白名单)与**漏放**(换个说法绕过黑名单)两种风险,且没有命中率统计来指导调词。
- 缓解方向:面板已能看到拦截记录,可定期复盘;长期可换成小模型二分类。
### 中:观测数据是单实例内存态
- RunLog 重启即清空、多实例部署时各自为政(面板只能看到所连实例的数据)、只保留最近 200 条。当前单实例部署下可接受,**水平扩容前必须重新设计**(落 Redis/DB 或接 Prometheus
- 同理,**并发限流的 16 是进程内信号量**:多实例时总并发 = 16 × N且 16 写死在 `router.go`,不可配置。
### 中:鉴权与暴露面
- SharedSecret 是**明文字符串比对**且和 KB 管理口令一起写在 `config.yaml``qiqi991012`),中间件里还硬编码了开发默认 JWT 密钥。生产必须换强随机值并走环境变量。
- `GET /models/active-config` **无鉴权**key 已脱敏只留尾号,但 provider/model/URL 对外可见)。
- CORS 是 `Allow-Origin: *`panic recovery 会把 recovered 内容原样返回给客户端(可能泄露内部路径/变量名)。
### 中:超时与重试的边界情况
- `WriteTimeout=300s` 是兜底,但 ReactLoop 本身**没有整体 deadline**:多轮迭代 + 每轮重试 1 次 + fallback 链,极端情况下可能逼近甚至顶到 300s届时连接被强制掐断、PHP 只收到断连而非结构化错误。
- `isRetryableNetworkError` 靠**错误文案子串匹配**"timeout"、"connection reset"…Go 版本升级或客户端封装改文案就可能失效——这是无类型错误链下的妥协,脆弱但可用。
### 低:其他已知瑕疵
- `tool_calls[].id` 缺失时用 `time.Now().UnixNano()` 生成,理论上高并发同纳秒会撞(概率极低)。
- KB V1 是 NoopEmbedder 纯关键词检索,召回质量有限,`ai_kb_source=local` 下检索增强效果打折V2 接 BGE-M3 前这是已知取舍。
- 60s 配置缓存意味着**不调 invalidate-cache 时切换最多延迟 1 分钟**——依赖 PHP 后台保存后主动调那个接口。
- 无 Prometheus/metrics 端点,告警只能靠日志和面板人工看。
---
## 四、优势(为什么这样设计)
### 1. 配置热加载 + 指纹缓存:切模型不重启
后台改 `xk_system_config` → Go 下一次请求自动用新配置60s 内,或调 invalidate 立即),客户端按指纹复用/重建。对比「启动时读一次配置」的方案,运维成本降了一个量级,这也是本项目踩坑(模型不一致)后重构出来的核心能力。
### 2. 多层防跑题:守卫 → 锚定 → 计划校验 → 反思
入口医疗守卫0 token 拦非医疗、Planning prompt 医疗锚定、计划输出不含医疗词即丢弃、Reflection 自检——四层针对「轻量模型易被通用指令带偏」的现实问题层层设防。单层都可能漏,叠加后 Lite 这类模型也能稳定输出医疗内容。
### 3. 故障半径控制:每一层都有降级路径
- 请求层:信号量满 → 503 快速失败 → PHP 走直连降级,不排队堆积;
- 网络层:瞬时抖动自动重试 1 次;
- 模型层:主模型挂 → FallbackChain 自动切备用;
- 配置层DB 抖动 → 回落 yaml/env 兜底配置;
- 进程层panic recovery + 优雅停机 + 四组 HTTP 超时防慢连接。
### 4. 全链路可观测,且观测不依赖被观测对象
每一步(守卫/检索/plan/llm_call/tool/reflection都有独立的耗时、token、detail实时看 Go 内存 RunLog + `/agent/view` 面板DB 挂了面板照常工作),历史审计查 PHP `xk_ai_generation_step` 表。排障时能精确回答「哪一步、花了多久、模型说了什么」。
### 5. 协议完整性:换更强模型零改动
Function Calling 按 OpenAI 完整协议实现(`tool_calls[].id``tool_call_id` 回传、`arguments` JSON 字符串),消息序列符合严格校验;今天用 Lite自动降级纯文本明天切 Pro/Max/DeepSeek 直接享受工具调用,不用再动协议层。
### 6. 安全默认值
API Key AES 加密入库(与 PHP 同算法)、日志默认不打请求体(`ai_agent_debug_log` 显式开启才打,防 PHI 泄漏、active-config 接口 key 脱敏只留尾号。
---
## 五、后续建议(按投入产出比排序)
1. **Reflection 闭环**:审核不合格时自动重试一次或 fallback 到更强模型(隐患一的直接解法)。
2. **ReactLoop 整体 deadline**:用 `context.WithTimeout`(如 240s包住整个循环保证永远先于 WriteTimeout 结构化返回。
3. **限流阈值进配置**:把 16 挪到 `xk_system_config` 或 config.yaml。
4. 生产部署前:换强随机 SharedSecret、收紧 CORS、active-config 加鉴权、panic 响应脱敏。
5. 多实例化时RunLog 与限流改集中式Redis或直接接 Prometheus + Grafana 替代自建面板。