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

14 KiB
Raw Permalink Blame History

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.Setuphttp.Server 带四组超时;优雅停机
internal/config YAML/env 静态配置 只做启动期兜底;运行期以 DB 为准
internal/dao 数据库访问 LoadActiveLLMConfig60s 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_agentPHP 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 发起「开方建议」为例:

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 记忆:当前 Tabagent_view_tab)、鉴权 tokenagent_view_token)、自动刷新开关(agent_view_refresh5s 轮询)、主题与 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.yamlqiqi991012),中间件里还硬编码了开发默认 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[].idtool_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 替代自建面板。