224 lines
14 KiB
Markdown
224 lines
14 KiB
Markdown
# 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_config(DB,运行期实时) > 环境变量 > 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 替代自建面板。
|