Files
xk-ai-agent/internal/agent/emr_agent.go
2026-08-14 21:50:48 +08:00

205 lines
7.0 KiB
Go
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.
package agent
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
)
// ========================================================================
// 病历生成 Agent (EMR Generator)
// ========================================================================
// 场景说明:
// 患者提供主诉和病史 → Agent 生成符合《中医病历书写规范》的结构化病历
//
// 模型路由:
// 默认路由到 "emr-generator" → 配置中指向 DeepSeek/gpt-4o 等强推理模型
//
// 生命周期:
// ① 感知:接收患者主诉+病史
// ② 规划:检索病历模板 → 标准化术语 → 生成初稿
// ③ 检索:调 MaxKB 获取病历书写规范
// ④ 工具:查 HIS 获取既往病史
// ⑤ 反思:规则引擎质控 → 不通过则修正
// ⑥ 输出:结构化病历 JSON
// ========================================================================
// EMRGenerator 病历生成 Agent
type EMRGenerator struct {
runner *Runner
scene string // 场景名(用于模型路由)
}
// EMRRequest 病历生成请求
type EMRRequest struct {
PatientID string `json:"patient_id" binding:"required"` // 患者 ID
ChiefComplaint string `json:"chief_complaint" binding:"required"` // 主诉
HistoryNotes string `json:"history_notes"` // 病史补充
Allergies []string `json:"allergies"` // 过敏史
PastIllness []string `json:"past_illness"` // 既往病史
}
// EMRResponse 病历生成响应
type EMRResponse struct {
SessionID string `json:"session_id"`
Draft string `json:"draft"` // 病历草稿(自然语言)
Structured string `json:"structured"` // 结构化 JSON
Issues []string `json:"issues"` // 质控问题
Status string `json:"status"` // success / need_revision
}
// NewEMRGenerator 创建病历生成 Agent
//
// 参数:
// runner - Agent 引擎(提供模型路由、工具、会话管理)
// scene - 场景名(对应 config.yaml 中 routes 的 key为空则用默认
func NewEMRGenerator(runner *Runner, scene string) *EMRGenerator {
if scene == "" {
scene = "emr-generator" // 默认场景名
}
return &EMRGenerator{
runner: runner,
scene: scene,
}
}
// Generate 执行病历生成(完整 Agent 生命周期)
func (g *EMRGenerator) Generate(ctx context.Context, req *EMRRequest) (*EMRResponse, error) {
// ===== ① 感知阶段 =====
// 创建会话绑定场景名Runner 会根据场景路由到对应模型)
session := g.runner.CreateSession(req.PatientID, g.scene)
log.Printf("[病历Agent] 开始 | 患者:%s 会话:%s 场景:%s",
req.PatientID, session.ID, g.scene)
// 注入系统提示词(约束 LLM 行为)
systemPrompt := `你是一位资深的中医主治医师,擅长书写规范化的中医门诊病历。
请严格按照以下结构输出病历:
1. 主诉Chief Complaint简洁概括主要症状+持续时间
2. 现病史History of Present Illness按时间线描述病情演变
3. 既往史Past History既往疾病、手术、过敏
4. 舌象Tongue舌质、舌苔描述
5. 脉象Pulse脉象特征
6. 辨证分析Pattern Differentiation八纲辨证+脏腑辨证
7. 西医诊断Western Diagnosis
8. 中医诊断TCM Diagnosis证型
要求:术语规范,使用标准中医术语,不遗漏关键信息。`
session.History = append(session.History, Message{
Role: "system", Content: systemPrompt, Timestamp: 0,
})
// 构造用户输入
userInput := fmt.Sprintf(`患者主诉:%s
患者补充病史:%s
过敏史:%v
既往病史:%v
请生成完整中医病历。`, req.ChiefComplaint, req.HistoryNotes, req.Allergies, req.PastIllness)
// ===== ②~⑤ Agent 推理循环 =====
// Runner.Run 内部会:
// 1. 通过 ModelRouter 获取当前场景的 LLM
// 2. LLM 自主决定调用哪些工具MaxKB 检索、HIS 查询等)
// 3. 工具结果喂回 LLM循环直到产出最终答案
result, err := g.runner.Run(ctx, session.ID, userInput)
if err != nil {
return nil, fmt.Errorf("[病历Agent] 执行失败: %w", err)
}
// ===== ⑥ 规则引擎质控(独立于 LLM 的硬校验) =====
issues := checkEMRQuality(result)
resp := &EMRResponse{
SessionID: session.ID,
Draft: result,
Issues: issues,
}
// ===== ⑦ 反思阶段 =====
if len(issues) > 0 {
// 有问题 → 让 Agent 基于反馈修正
log.Printf("[病历Agent] 质控发现问题,进入反思修正 | 问题:%v", issues)
revisionInput := fmt.Sprintf("请修正以下病历中的问题:\n%s\n\n原病历\n%s",
strings.Join(issues, "\n"), result)
revised, err := g.runner.Run(ctx, session.ID, revisionInput)
if err == nil {
resp.Draft = revised
resp.Issues = checkEMRQuality(revised)
}
resp.Status = "need_revision"
} else {
resp.Status = "success"
}
// 提取结构化字段
resp.Structured = extractStructuredEMR(resp.Draft)
log.Printf("[病历Agent] 完成 | 会话:%s 状态:%s 问题数:%d",
session.ID, resp.Status, len(resp.Issues))
return resp, nil
}
// ========================================================================
// 内部辅助函数
// ========================================================================
// checkEMRQuality 病历质控检查
//
// 注意:这里只做简单的文本检查。
// 生产环境应调用 rule 包的 EMRQualityChecker更完整的规则集
func checkEMRQuality(emrText string) []string {
issues := make([]string, 0)
requiredFields := []string{"主诉", "舌", "脉", "诊断"}
for _, field := range requiredFields {
if !strings.Contains(emrText, field) {
issues = append(issues, fmt.Sprintf("【缺失】病历缺少「%s」字段", field))
}
}
// 口语术语检查
slangMap := map[string]string{
"胃不舒服": "胃脘不适", "头晕": "眩晕", "心慌": "心悸",
"睡不着": "失眠", "吃不下": "纳差", "拉肚子": "泄泻",
}
for slang, standard := range slangMap {
if strings.Contains(emrText, slang) {
issues = append(issues, fmt.Sprintf("【术语】建议将「%s」改为「%s」", slang, standard))
}
}
return issues
}
// extractStructuredEMR 从自然语言病历中提取结构化字段
func extractStructuredEMR(draft string) string {
structured := map[string]string{
"chief_complaint": extractField(draft, "主诉"),
"tongue": extractField(draft, "舌象"),
"pulse": extractField(draft, "脉象"),
"diagnosis_tcm": extractField(draft, "中医诊断"),
"diagnosis_wm": extractField(draft, "西医诊断"),
}
b, _ := json.Marshal(structured)
return string(b)
}
// extractField 简单提取字段值(生产环境建议用 LLM + NER
func extractField(text, fieldName string) string {
lines := strings.Split(text, "\n")
for _, line := range lines {
if strings.Contains(line, fieldName) {
parts := strings.SplitN(line, "", 2)
if len(parts) == 2 {
return strings.TrimSpace(parts[1])
}
}
}
return ""
}