14 KiB
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)。
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(PHPAiAgentFactory读取),与「用哪个模型」彻底解耦——这是踩过坑后的关键设计:早期把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 发起「开方建议」为例:
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
关键细节:
- 守卫在最前面:不花任何 token 就能拒掉「PHP 组装出错 / 接口被滥用」的非医疗请求,同时留下
medical_guard步骤便于定位是谁的问题。 - 每次调用都实时解析模型:不是启动时定死,后台切模型不用重启 Go。
- 消息序列保证以 user/tool 结尾:Planning 注入后补 user 帧,兼容 Spark Lite 等严格遵循 OpenAI 协议的模型(曾因 assistant 结尾触发 10003)。
- 网络错误自动重试 1 次(间隔 1s):只重试连接/超时类错误,「API 返回 xxx」业务错误不重试,避免双倍烧 token。
- 无论成败都写 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 脱敏只留尾号。
五、后续建议(按投入产出比排序)
- Reflection 闭环:审核不合格时自动重试一次或 fallback 到更强模型(隐患一的直接解法)。
- ReactLoop 整体 deadline:用
context.WithTimeout(如 240s)包住整个循环,保证永远先于 WriteTimeout 结构化返回。 - 限流阈值进配置:把 16 挪到
xk_system_config或 config.yaml。 - 生产部署前:换强随机 SharedSecret、收紧 CORS、active-config 加鉴权、panic 响应脱敏。
- 多实例化时:RunLog 与限流改集中式(Redis),或直接接 Prometheus + Grafana 替代自建面板。