AI医疗分诊与健康咨询助手agent开发——(2)让AI输出可控:结构化分诊与安全规则

上一篇: AI医疗分诊与健康咨询助手agent开发------(1)从零搭建SpringBoot与AI对话系统:后端骨架 + 前端对话页 + SSE流式输出

下一篇:AI医疗分诊与健康咨询助手agent开发------(3)从零到一,终于搞懂了 Agent 是什么?

前面搞定了基础对话,但AI回的话没法直接用------它给了一些建议,初步建议该挂什么科,还会说头疼的问题有很多。如果没有约束规则,AI甚至还会"建议你吃XXX药"。这哪行?

医疗场景下,AI自由发挥太危险了。用户输入"胸口疼",AI可能回一段500字的健康科普,也可能直接告诉你"可能是心梗,建议服用阿司匹林"。前者没用,后者要命。

所以这一篇,我主要解决两个问题:让AI输出结构化JSON ,以及在AI前后加上安全网。顺带把多轮追问也做了**(分诊的功能**),因为用户只说"头疼"你就推荐科室,也太草率了。


一、让AI输出结构化JSON

为什么需要结构化输出

AI返回自然语言,前端没法用。你说"建议挂心内科",前端要展示什么?科室标签、紧急程度标签、置信度进度条......这些都需要结构化字段。

更关键的是,自然语言没法做规则判断。你没法从一段话里可靠地提取"这个用户是不是急症"。结构化输出是后续所有逻辑的基础。

TriageResponse的设计

这是最终返回给前端的结构体:

java 复制代码
@Data @Builder
public class TriageResponse {
    private boolean needEmergency;           // 是否需要急诊
    private String urgencyLevel;             // IMMEDIATE/URGENT/OUTPATIENT/OBSERVATION
    private String primaryDepartment;        // 首选科室
    private List<String> alternativeDepartments; // 备选科室
    private double confidenceScore;          // 0-1置信度
    private boolean needClarification;       // 是否需要追问
    private List<String> clarificationQuestions; // 追问问题列表
    private String reasoningSummary;         // 推理过程简述
    private List<String> preparationAdvice;  // 就诊前准备建议
    private List<String> warningSigns;       // 需警惕的危险信号
    private String disclaimer;               // 免责声明
    private String sessionId;                // 会话ID
    private String stage;                    // 当前阶段
    private int clarificationRound;          // 当前追问轮次
}

字段不少,但每个都有用。needEmergencyurgencyLevel决定前端是弹红色警告还是普通展示;needClarificationclarificationQuestions驱动追问流程;confidenceScore让前端知道这个推荐靠不靠谱。

紧急程度分4级,从轻到重:

  • OBSERVATION(观察):症状轻微,可以先自己观察观察,比如偶尔轻微头痛
  • OUTPATIENT(门诊):建议去门诊看看,不紧急,比如持续头痛伴恶心
  • URGENT(急诊):建议去急诊,比如剧烈头痛伴呕吐
  • IMMEDIATE(立即急救):别犹豫了,直接打120,比如胸痛+呼吸困难

注意没有"EMERGENCY"这个级别------急症不是紧急程度的一个级别,而是一种特殊状态,命中急症规则后会直接走急症流程,不走分诊。

Prompt怎么约束AI返回JSON

核心就是在System Prompt里硬性规定输出格式,然后给一个示例:

复制代码
你必须返回严格的JSON格式,不要包含任何其他文字。
返回格式如下:
{
  "needEmergency": boolean,
  "urgencyLevel": "IMMEDIATE|URGENT|OUTPATIENT|OBSERVATION",
  "primaryDepartment": "科室名称",
  ...
}

听起来很美好对吧?实际跑起来就知道,AI根本不老实。

IntakeAgent的Prompt核心约束

举个小例子,IntakeAgent的Prompt核心约束长这样:

typescript 复制代码
  /**
     * IntakeAgent系统提示词
     */
    @Bean
    public String intakeAgentPrompt() {
        return """
            你是一名专业的医疗接诊助手。你的任务是收集患者的症状信息,判断信息是否充分。

            【需要收集的信息项及权重】
            | 信息项 | 权重 | 优先级 | 说明 |
            |--------|------|--------|------|
            | 主要不适部位和具体感受 | 25% | 必须 | 如"头痛""胸口闷" |
            | 症状持续时间 | 20% | 必须 | 如"3天""2小时" |
            | 症状严重程度 | 20% | 必须 | 如"轻微""剧烈""影响睡眠" |
            | 伴随症状 | 15% | 必须 | 如"伴恶心""伴发热" |
            | 年龄和性别 | 10% | 重要 | 影响科室倾向 |
            | 既往病史 | 5% | 参考 | 相关慢性病史 |
            | 近期用药情况 | 5% | 参考 | 当前服用的药物 |

            【infoCompleteness计算规则】
            根据用户已提供的信息,按权重累加得分:
            - 该信息项已明确提供 → 加该项满分权重
            - 该信息项部分提供(如只说"头痛"没说具体感受) → 加该项一半权重
            - 该信息项未提供 → 加0
            最终得分为各项累加之和,即为infoCompleteness值。

            示例:用户说"我头痛3天了,有点恶心"
            - 部位(25%) + 持续时间(20%) + 伴随症状(15%) = 0.60
            - 严重程度未提供(0) + 年龄性别未提供(0) + 既往病史未提供(0) + 用药未提供(0)
            - infoCompleteness = 0.60

            【infoCompleteness分档】
            - 0.8-1.0: 核心信息齐全,可以进行分诊,needClarification=false
            - 0.5-0.8: 缺少部分核心信息,需追问1-2个问题,needClarification=true
            - 0.0-0.5: 核心信息严重不足,需追问关键问题,needClarification=true

            【输出要求】
            请严格按照以下JSON格式返回,不要包含任何其他文字:

            {
              "infoCompleteness": 0.0-1.0,
              "needClarification": false,
              "missingInfo": ["缺失的信息项名称"],
              "clarificationQuestions": ["追问的问题"],
              "symptomSummary": "标准化后的症状描述"
            }

            【追问规则】
            - 每次最多生成2个追问问题
            - 优先追问权重最高的缺失项(严重程度 > 持续时间 > 伴随症状 > 年龄性别 > 既往病史 > 近期用药情况)
            - 患者已经提供的信息、已经回答的问题,不要重复问
            - 不要问跟症状无关的隐私

            【绝对红线】
            - 绝不猜测或暗示疾病名称
            - 绝不建议药物或治疗方案
            - 绝不用"你可能得了..."这种话

            请用中文回复。
            """;
    }

你看,Prompt里把"必须做什么"和"绝对不能做什么"写得清清楚楚。AI就像个实习生,你不说清楚它就乱来。

踩坑:AI返回的JSON经常格式不对

【其实这里已经再提示词中对AI进行约束,告诉AI要返回什么格式的,但是为了避免AI不按要求来,还是要用StructuredOutputParser容错进行兜底】

这是我遇到的三种典型情况:

1. 被Markdown代码块包裹

AI返回的是:

复制代码
根据症状分析,建议如下:
```json
{"needEmergency": false, ...}


前面带一段废话,JSON还被` ```json ````包着。直接用ObjectMapper解析必炸。

2. 缺少字段

AI有时候会"自作主张"省略它觉得不重要的字段,比如alternativeDepartments直接不返回。Jackson默认遇到null字段会报错。

3. 直接返回自然语言

偶尔AI完全不按格式来,直接回一句"根据您的症状,建议您前往神经内科就诊"。连JSON的影子都没有。

StructuredOutputParser的容错策略

针对上面的问题,我写了一个解析器,策略是:提取 -> 解析 -> 填充默认值 -> 兜底。

java 复制代码
/**
 * 结构化输出解析器
 * 将AI模型输出解析为结构化分诊响应
 */
@Component
public class StructuredOutputParser {

    private static final Logger log = LoggerFactory.getLogger(StructuredOutputParser.class);

    private final ObjectMapper objectMapper;

    //ObjectMapper要配置成忽略未知属性、允许缺失字段:
    public StructuredOutputParser() {
        this.objectMapper = new ObjectMapper();
        this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    }

    /**
     * 将AI模型响应解析为TriageResponse
     *
     * @param aiOutput AI模型原始输出
     * @return 解析后的TriageResponse或降级响应
     */
    public TriageResponse parse(String aiOutput) {
        if (aiOutput == null || aiOutput.isBlank()) {
            log.warn("AI output is null or blank, returning low confidence fallback");
            return TriageResponse.lowConfidenceFallback("AI模型返回为空,建议前往全科门诊就诊。");
        }

        try {
            // 尝试从输出中提取JSON
            String json = extractJson(aiOutput);

            if (json == null) {
                log.warn("No JSON found in AI output, returning low confidence fallback");
                return TriageResponse.lowConfidenceFallback("AI模型返回格式异常,建议前往全科门诊就诊。");
            }

            // 解析JSON
            TriageResponse response = objectMapper.readValue(json, TriageResponse.class);

            // 验证并设置默认值
            return validateAndSetDefaults(response);

        } catch (JsonProcessingException e) {
            log.error("Failed to parse AI output as JSON: {}", e.getMessage());
            return TriageResponse.lowConfidenceFallback("AI模型返回JSON解析失败,建议前往全科门诊就诊。");
        } catch (Exception e) {
            log.error("Unexpected error parsing AI output: {}", e.getMessage(), e);
            return TriageResponse.lowConfidenceFallback("处理AI模型响应时发生异常,建议前往全科门诊就诊。");
        }
    }

    /**
     * 从AI输出中提取JSON字符串
     * 处理JSON被markdown代码块或其他文本包裹的情况
     */
    public String extractJson(String output) {
        // 首先尝试在代码块中查找JSON
        Pattern codeBlockPattern = Pattern.compile("```(?:json)?\\s*\\n?(\\{[\\s\\S]*?\\})\\s*\\n?```");
        Matcher codeBlockMatcher = codeBlockPattern.matcher(output);
        if (codeBlockMatcher.find()) {
            return codeBlockMatcher.group(1);
        }

        // 尝试直接查找JSON对象
        Pattern jsonPattern = Pattern.compile("\\{[\\s\\S]*\\}");
        Matcher jsonMatcher = jsonPattern.matcher(output);
        if (jsonMatcher.find()) {
            return jsonMatcher.group();
        }

        return null;
    }

    /**
     * 验证并为缺失字段设置默认值
     */
    private TriageResponse validateAndSetDefaults(TriageResponse response) {
        // 如果缺少免责声明则设置默认值
        if (response.getDisclaimer() == null || response.getDisclaimer().isBlank()) {
            response.setDisclaimer("本系统仅提供就诊方向参考,不构成医疗诊断。");
        }

        // 如果缺少状态则设置默认值
        if (response.getStatus() == null || response.getStatus().isBlank()) {
            response.setStatus("success");
        }

        // 验证紧急程度
        if (response.getUrgencyLevel() == null || response.getUrgencyLevel().isBlank()) {
            response.setUrgencyLevel("OUTPATIENT");
        } else {
            String level = response.getUrgencyLevel().toUpperCase();
            if (!List.of("OBSERVATION", "OUTPATIENT", "URGENT", "IMMEDIATE").contains(level)) {
                // 非法值,降级为门诊
                response.setUrgencyLevel("OUTPATIENT");
            } else {
                response.setUrgencyLevel(level);
            }
        }

        // 验证置信度分数
        if (response.getConfidenceScore() < 0 || response.getConfidenceScore() > 1) {
            response.setConfidenceScore(0.5);
        }

        // 如果缺少主推科室则设置默认值
        if (response.getPrimaryDepartment() == null || response.getPrimaryDepartment().isBlank()) {
            response.setPrimaryDepartment("全科门诊");
        }

        // 如果列表为null则设置默认空列表
        if (response.getAlternativeDepartments() == null) {
            response.setAlternativeDepartments(new ArrayList<>());
        }
        if (response.getClarificationQuestions() == null) {
            response.setClarificationQuestions(new ArrayList<>());
        }
        if (response.getPreparationAdvice() == null) {
            response.setPreparationAdvice(new ArrayList<>());
        }
        if (response.getWarningSigns() == null) {
            response.setWarningSigns(new ArrayList<>());
        }

        return response;
    }
}

这里有个细节:ObjectMapper要配置成忽略未知属性、允许缺失字段:

java 复制代码
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

不然AI多返回一个你没定义的字段,解析直接报错。

lowConfidenceFallback()返回一个置信度为0.3的兜底结果,前端看到低置信度会提示用户"建议重新描述症状"。总比页面报错强。

java 复制代码
   /**
     * 创建低置信度降级响应(默认兜底)
     */
    public static TriageResponse lowConfidenceFallback(String reasoningSummary) {
        return TriageResponse.builder()
                .needEmergency(false)
                .urgencyLevel("OUTPATIENT")
                .primaryDepartment("全科门诊")
                .alternativeDepartments(List.of())
                .confidenceScore(0.3)
                .needClarification(false)
                .clarificationQuestions(List.of())
                .reasoningSummary(reasoningSummary)
                .preparationAdvice(List.of("建议携带既往病历资料", "记录症状发作时间"))
                .warningSigns(List.of("症状加重", "出现新症状"))
                .disclaimer("本系统仅提供就诊方向参考,不构成医疗诊断。")
                .status("success")
                .build();
    }

二、急症规则------AI之前的安全网

为什么规则要在AI前面

AI慢。一次API调用少说1-2秒,多的话5-6秒。用户输入"意识不清、大出血",你还要等AI返回JSON再判断是不是急症?人命等不起。

所以我的方案是:规则引擎在前,AI在后。用户输入先过规则引擎,命中急症关键词就直接返回,根本不调AI。只有规则引擎没命中的,才走AI分诊。

EmergencyRuleService设计

规则分两类:单关键词和组合关键词。【这里先用硬编码的形式,先看到效果,后续肯定是要有一个类似知识库的东西,不能直接再代码中写死!!】

java 复制代码
/**
 * 急症规则服务
 * 在调用AI模型前检查急症症状
 */
@Service
public class EmergencyRuleService {

    private static final Logger log = LoggerFactory.getLogger(EmergencyRuleService.class);

    /**
     * 急症规则模式
     * 每个条目包含必须全部出现才能触发的关键词列表
     */
    private static final List<List<String>> EMERGENCY_RULES = List.of(
            // 胸痛 + 呼吸困难
            List.of("胸痛", "呼吸困难"),
            List.of("胸口疼", "喘不过气"),
            List.of("胸闷", "气短"),
            List.of("心口疼", "呼吸困难"),

            // 意识不清
            List.of("意识不清"),
            List.of("昏迷"),
            List.of("失去意识"),
            List.of("晕倒"),
            List.of("晕厥"),

            // 大出血
            List.of("大出血"),
            List.of("出血不止"),
            List.of("血流不止"),
            List.of("大量出血"),

            // 自杀倾向
            List.of("自杀"),
            List.of("不想活"),
            List.of("轻生"),
            List.of("想死"),

            // 剧烈头痛 + 肢体无力
            List.of("剧烈头痛", "肢体无力"),
            List.of("头痛剧烈", "手脚无力"),
            List.of("头疼厉害", "无力"),

            // 剧烈腹痛 + 持续呕吐
            List.of("剧烈腹痛", "持续呕吐"),
            List.of("肚子剧痛", "呕吐不止"),
            List.of("腹痛剧烈", "一直吐"),

            // 心脏相关急症
            List.of("心脏骤停"),
            List.of("心肺复苏"),
            List.of("心梗"),
            List.of("心肌梗塞"),

            // 中风症状
            List.of("中风"),
            List.of("脑梗"),
            List.of("脑出血"),
            List.of("口角歪斜"),
            List.of("言语不清", "肢体无力"),

            // 过敏性休克
            List.of("过敏性休克"),
            List.of("过敏", "呼吸困难"),
            List.of("过敏", "休克"),

            // 其他急症
            List.of("高热", "抽搐"),
            List.of("体温", "40"),
            List.of("高烧", "抽搐"),
            List.of("触电"),
            List.of("溺水"),
            List.of("中毒"),
            List.of("窒息")
    );

    /**
     * 单关键词急症触发器
     */
    private static final List<String> SINGLE_KEYWORD_EMERGENCIES = List.of(
            "心脏骤停",
            "心肺复苏",
            "自杀",
            "轻生",
            "不想活",
            "想死",
            "触电",
            "溺水",
            "中毒",
            "窒息",
            "大出血",
            "出血不止"
    );

    /**
     * 检查消息是否包含急症症状
     *
     * @param message 患者症状描述
     * @return 包含是否检测到急症及原因的EmergencyCheckResult
     */
    public EmergencyCheckResult checkEmergency(String message) {
        if (message == null || message.isBlank()) {
            return EmergencyCheckResult.noEmergency();
        }

        String lowerMessage = message.toLowerCase();

        // 首先检查单关键词急症
        for (String keyword : SINGLE_KEYWORD_EMERGENCIES) {
            if (lowerMessage.contains(keyword.toLowerCase())) {
                log.warn("Emergency detected with single keyword: {}", keyword);
                return EmergencyCheckResult.emergency(
                        "检测到急症关键词:" + keyword + "。请立即拨打120或前往最近的急诊科!"
                );
            }
        }

        // 检查多关键词急症
        for (List<String> rule : EMERGENCY_RULES) {
            boolean allMatch = rule.stream()
                    .allMatch(keyword -> lowerMessage.contains(keyword.toLowerCase()));

            if (allMatch) {
                log.warn("Emergency detected with rule: {}", rule);
                return EmergencyCheckResult.emergency(
                        "检测到急症症状组合:" + String.join("、", rule) + "。请立即拨打120或前往最近的急诊科!"
                );
            }
        }

        return EmergencyCheckResult.noEmergency();
    }

    /**
     * 急症检查结果
     */
    public static class EmergencyCheckResult {
        private final boolean isEmergency;
        private final String message;

        private EmergencyCheckResult(boolean isEmergency, String message) {
            this.isEmergency = isEmergency;
            this.message = message;
        }

        public static EmergencyCheckResult emergency(String message) {
            return new EmergencyCheckResult(true, message);
        }

        public static EmergencyCheckResult noEmergency() {
            return new EmergencyCheckResult(false, null);
        }

        public boolean isEmergency() {
            return isEmergency;
        }

        public String getMessage() {
            return message;
        }
    }
}

为什么要有组合关键词?因为"胸痛"单独出现不一定急症,可能是肌肉拉伤;但"胸痛"+"呼吸困难"同时出现,大概率是心肺问题,必须按急症处理。

命中后直接返回,不调AI。响应时间从2秒降到2毫秒。

在Orchestrator中的调用顺序

java 复制代码
/**
     * 输入:用户消息 + 会话上下文
     * 输出:是否急症、信息完整度、追问问题、症状摘要
     * @param input Agent输入
     * @return
     */
    @Override
    public IntakeOutput execute(IntakeInput input) {
        long startTime = System.currentTimeMillis();
        String inputSummary = "用户消息: " + truncate(input.getUserMessage(), 100);

        try {
            // Step 1: 先用规则检查急症(不调AI,速度快)
            EmergencyRuleService.EmergencyCheckResult emergencyResult =
                    emergencyRuleService.checkEmergency(input.getUserMessage());

            if (emergencyResult.isEmergency()) {
                log.warn("[IntakeAgent] 急症检测命中: {}", emergencyResult.getMessage());
                IntakeOutput output = IntakeOutput.builder()
                        .emergencyDetected(true)
                        .emergencyMessage(emergencyResult.getMessage())
                        .needClarification(false)
                        .infoCompleteness(1.0)
                        .symptomSummary(input.getUserMessage())
                        .build();

                recordTrace(inputSummary, "急症检测命中", startTime, true, null);
                return output;
            }

            // Step 2: 调用AI判断信息完整度
            log.info("[IntakeAgent] 调用AI判断信息完整度");
            String contextSummary = buildContextForIntake(input.getSession(), input.getUserMessage());
            String aiOutput = aiChatClient.chat(intakePrompt, contextSummary);

            // Step 3: 解析AI输出
            IntakeOutput output = parseIntakeOutput(aiOutput, input.getUserMessage());

            log.info("[IntakeAgent] 信息完整度: {}, 需要追问: {}",
                    output.getInfoCompleteness(), output.isNeedClarification());

            recordTrace(inputSummary,
                    String.format("完整度:%.2f, 追问:%s", output.getInfoCompleteness(), output.isNeedClarification()),
                    startTime, true, null);
            return output;

        } catch (Exception e) {
            log.error("[IntakeAgent] 执行异常: {}", e.getMessage(), e);
            IntakeOutput fallback = IntakeOutput.builder()
                    .emergencyDetected(false)
                    .needClarification(false)
                    .infoCompleteness(0.5)
                    .symptomSummary(input.getUserMessage())
                    .build();

            recordTrace(inputSummary, "执行异常: " + e.getMessage(), startTime, false, e.getMessage());
            return fallback;
        }
    }

这个顺序很重要。规则引擎是第一道防线,AI是第二道。千万别反过来。


三、安全过滤------AI说了不该说的怎么办

禁词列表

AI有时候会"越界",输出一些医疗场景下不该说的话。我列了一个禁词表:

  • 诊断类:"确诊"、"诊断为"、"你患有"
  • 处方类:"建议服用"、"处方"、"用药"
  • 药物剂量类:"mg"、"ml"(在药物上下文中)
  • 治疗方案类:"治疗方案"、"手术方式"

validateResponse方法

AI返回结果后,过一遍安全过滤**【这里先用硬编码的形式,后续会优化!!】**:

java 复制代码
 /**
     * 安全过滤:检查并替换禁止内容
     */
    private TriageResponse validateResponse(TriageResponse response) {
        // 安全过滤:将禁止内容替换为安全表述,而非整段丢弃
        String summary = response.getReasoningSummary();
        if (summary != null) {
            String filtered = sanitizeText(summary);
            if (!filtered.equals(summary)) {
                log.warn("[TriageAgent] reasoningSummary包含禁止内容,已替换为安全表述");
                response.setReasoningSummary(filtered);
            }
        }

        if (response.getPreparationAdvice() != null) {
            response.setPreparationAdvice(response.getPreparationAdvice().stream()
                    .map(this::sanitizeText)
                    .toList());
        }

        if (response.getWarningSigns() != null) {
            response.setWarningSigns(response.getWarningSigns().stream()
                    .map(this::sanitizeText)
                    .toList());
        }

        return response;
    }
    /**
     * 将文本中的禁止内容替换为安全表述,保留其余内容不变
     */
    private String sanitizeText(String text) {
        if (text == null) return null;

        // 诊断性表述 → 安全替代
        text = text.replaceAll("确诊为(.+?)([,。,\\.\\s]|$)", "症状与$1相关,请就医确认$2");
        text = text.replaceAll("诊断为(.+?)([,。,\\.\\s]|$)", "症状提示$1可能,请就医确认$2");
        text = text.replaceAll("你可能得了(.+?)([,。,\\.\\s]|$)", "症状可能与$1相关,请就医确认$2");
        text = text.replaceAll("你患有(.+?)([,。,\\.\\s]|$)", "存在$1相关症状,请就医确认$2");
        text = text.replaceAll("你患了(.+?)([,。,\\.\\s]|$)", "出现$1相关症状,请就医确认$2");
        text = text.replaceAll("你患的是(.+?)([,。,\\.\\s]|$)", "症状与$1相关,请就医确认$2");
        text = text.replaceAll("这是(.+?)病的症状", "这些症状与$1相关");

        // 处方性表述 → 安全替代
        text = text.replaceAll("建议服用(.+?)([,。,\\.\\s]|$)", "请咨询医生是否需要使用$1$2");
        text = text.replaceAll("推荐药物[::]?(.+?)([,。,\\.\\s]|$)", "请咨询医生关于$1等药物的使用$2");
        text = text.replaceAll("用量[为是]?(.+?)([,。,\\.\\s]|$)", "具体用量请遵医嘱$2");
        text = text.replaceAll("剂量[为是]?(.+?)([,。,\\.\\s]|$)", "具体剂量请遵医嘱$2");
        text = text.replaceAll("处方[::]?(.+?)([,。,\\.\\s]|$)", "请咨询医生获取处方$2");
        text = text.replaceAll("建议吃(.+?)([,。,\\.\\s]|$)", "请咨询医生是否需要服用$1$2");
        text = text.replaceAll("服用(\\d+\\.?\\d*)(mg|ml)", "遵医嘱使用$1$2");

        // 用药频率与周期 → 安全替代
        text = text.replaceAll("(每[日天][\\d一两三四五六七八九十1234567890]+次)", "用药频率请遵医嘱");
        text = text.replaceAll("(连续[\\d一两三四五六七八九十1234567890]+天)", "用药周期请遵医嘱");

        // 治疗方案 → 安全替代
        text = text.replaceAll("建议手术", "请咨询医生是否需要手术治疗");
        text = text.replaceAll("治疗方案[::]?(.+?)([,。,\\.\\s]|$)", "请咨询医生制定治疗方案$2");
        text = text.replaceAll("需要做(.+?)治疗", "请咨询医生是否需要$1治疗");

        return text;
    }

命中禁词后怎么处理?不是直接拦截,而是替换成安全表述。比如"确诊为高血压"改成"症状与高血压相关,请就医确认"。

免责声明自动追加

不管AI输出什么,所有回复必须带免责声明。这不是可选的,是必须的:

java 复制代码
private static final String DISCLAIMER_TEXT =
    "本系统仅提供分诊建议,不构成医疗诊断。请以医生面诊结果为准。如有紧急情况,请立即拨打120。";

// 构建TriageResponse时始终追加
.disclaimer(DISCLAIMER_TEXT)

踩坑:AI会绕过禁词

这是最头疼的。你禁了"确诊",AI改成"你的症状与XXX相符";你禁了"建议服用",AI改成"临床上常使用XXX来缓解此类症状"。

单纯的关键词匹配根本防不住。AI太聪明了,它会换着法子表达同一个意思。

我目前的做法是两层防御:

  1. 关键词过滤:拦截最明显的违规表述
  2. Prompt约束:在System Prompt里反复强调"你只能提供分诊建议,不能给出诊断、处方或治疗建议"

第二层其实更有效。与其事后过滤,不如从源头减少违规输出。但也不能完全依赖Prompt,所以两层都要有。

  • 后续考虑其他方面的的优化【关键词(快速过滤)+ Embedding(语义兜底)+ LLM(人工审核辅助】,用另一个AI来判断输出是否违规。
  • 但那是后话了,当前的关键词+Prompt方案先顶着。
  • 后续可能是,先预想一下:
    • 将硬编码正则提取到YAML配置文件,支持热更新
    • 引入数据库+本地缓存,建立规则管理后台
    • 接入语义识别,建立完整的安全过滤服务体系
    • 人工兜底?

四、多轮追问------信息不足别急着给结论

为什么需要追问

用户说"头疼",你就推荐神经内科?太草率了。头疼的原因多了去了------偏头痛、颈椎病、高血压、脑部病变......没有更多信息,推荐什么科室都不靠谱。

所以当信息不够的时候,系统应该追问,而不是硬着头皮给结论。

信息完整度怎么算

那"信息够不够"怎么判断?我定义了7项核心信息,每项有个权重:

信息项 权重 优先级 说明
主要不适部位和具体感受 25% 必须 如"头痛""胸口闷"
症状持续时间 20% 必须 如"3天""2小时"
症状严重程度 20% 必须 如"轻微""剧烈""影响睡眠"
伴随症状 15% 必须 如"伴恶心""伴发热"
年龄和性别 10% 重要 影响科室倾向
既往病史 5% 参考 相关慢性病史
近期用药情况 5% 参考 当前服用的药物

举个例子:用户只说了"头疼",那只有主要不适0.25分,完整度0.25,远低于0.6阈值,必须追问。用户说"头疼两天了,有点恶心",部位(25%) + 持续时间(20%) + 伴随症状(15%) = 0.60,大于等于0.6了,可以分诊了。

不过这个计算不是硬编码的,是让AI自己评估的。0.6阈值是硬性条件------AI说不够但完整度已经到0.7了,那就直接分诊,不再追问。

ConsultationSession会话模型

java 复制代码
/**
 * 会话模型实体类
 * 表示一个完整的问诊会话及其历史记录
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ConsultationSession {

    /**
     * 会话唯一标识
     */
    private String sessionId;

    /**
     * 当前问诊阶段
     */
    private ConsultationStage stage;

    /**
     * 追问轮次(0-3)
     */
    private int clarificationRound;

    /**
     * 最大追问轮次
     */
    private static final int MAX_CLARIFICATION_ROUNDS = 3;

    /**
     * 消息历史记录
     */
    private List<ConsultationMessage> messages;

    /**
     * 症状摘要
     */
    private String symptomSummary;

    /**
     * 信息完整度(0.0-1.0)
     */
    private double infoCompleteness;

    /**
     * 缺失信息列表
     */
    private List<String> missingInfo;

    /**
     * 标准化症状摘要
     */
    private String normalizedSummary;

    /**
     * 是否检测到急症
     */
    private boolean emergencyDetected;

    /**
     * 急症关键词列表
     */
    private List<String> emergencyKeywords;

    /**
     * 会话创建时间
     */
    private LocalDateTime createdAt;

    /**
     * 最后更新时间
     */
    private LocalDateTime updatedAt;

    /**
     * 创建新会话
     */
    public static ConsultationSession createNew() {
        LocalDateTime now = LocalDateTime.now();
        return ConsultationSession.builder()
                .sessionId(UUID.randomUUID().toString())
                .stage(ConsultationStage.INTAKE)
                .clarificationRound(0)
                .messages(new ArrayList<>())
                .symptomSummary("")
                .infoCompleteness(0.0)
                .missingInfo(new ArrayList<>())
                .normalizedSummary("")
                .emergencyDetected(false)
                .emergencyKeywords(new ArrayList<>())
                .createdAt(now)
                .updatedAt(now)
                .build();
    }

    /**
     * 添加消息到会话
     */
    public void addMessage(ConsultationMessage message) {
        if (messages == null) {
            messages = new ArrayList<>();
        }
        messages.add(message);
        updatedAt = LocalDateTime.now();
    }

    /**
     * 检查是否可以继续追问
     */
    public boolean canAskClarification() {
        return clarificationRound < MAX_CLARIFICATION_ROUNDS;
    }

    /**
     * 增加追问轮次
     */
    public void incrementClarificationRound() {
        clarificationRound++;
    }

    /**
     * 重置追问轮次(分诊完成后调用,允许新一轮追问)
     */
    public void resetClarificationRound() {
        clarificationRound = 0;
    }

    /**
     * 获取格式化的对话历史
     */
    public String getFormattedHistory() {
        if (messages == null || messages.isEmpty()) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        for (ConsultationMessage msg : messages) {
            sb.append(msg.getRole()).append(": ").append(msg.getContent()).append("\n");
        }
        return sb.toString();
    }

    /**
     * 获取最近N条消息用于上下文
     */
    public List<ConsultationMessage> getLastMessages(int n) {
        if (messages == null || messages.isEmpty()) {
            return new ArrayList<>();
        }
        int start = Math.max(0, messages.size() - n);
        return new ArrayList<>(messages.subList(start, messages.size()));
    }
}

会话阶段用枚举定义:

java 复制代码
/**
 * 会话阶段枚举
 * 表示医疗问诊的当前阶段
 */
public enum ConsultationStage {

    /**
     * 初始问诊 - 收集症状信息
     */
    INTAKE,

    /**
     * 追问阶段 - 询问补充问题
     */
    CLARIFICATION,

    /**
     * 分诊完成 - 提供科室推荐
     */
    TRIAGE,

    /**
     * 急症 - 检测到紧急症状
     */
    EMERGENCY,

    /**
     * 问诊结束
     */
    COMPLETED
}

追问轮次控制

在Orchestrator里,判断要不要追问的逻辑:

java 复制代码
    // 分支2: 追问流程(信息完整度低于阈值且AI建议追问且追问次数未达上限)
        double COMPLETENESS_THRESHOLD = 0.6;
        boolean infoInsufficient = intakeOutput.getInfoCompleteness() < COMPLETENESS_THRESHOLD
                || intakeOutput.isNeedClarification();
        if (infoInsufficient && session.canAskClarification()) {
            log.info("[Orchestrator] 信息不足,进入追问流程");
            session.incrementClarificationRound();
            sessionContextService.updateStage(session, ConsultationStage.CLARIFICATION);

            TriageResponse response = TriageResponse.builder()
                    .needEmergency(false)
                    .urgencyLevel("OBSERVATION")
                    .primaryDepartment("待定")
                    .alternativeDepartments(new ArrayList<>())
                    .confidenceScore(intakeOutput.getInfoCompleteness())
                    .needClarification(true)
                    .clarificationQuestions(intakeOutput.getClarificationQuestions())
                    .reasoningSummary("信息不足,需要进一步了解症状详情")
                    .preparationAdvice(new ArrayList<>())
                    .warningSigns(new ArrayList<>())
                    .disclaimer("本系统仅提供就诊方向参考,不构成医疗诊断。")
                    .status("success")
                    .build();

            response.setAgentTrace(traces);
            setSessionInfo(response, session);

            // 记录助手消息
            String assistantContent = String.join("; ", intakeOutput.getClarificationQuestions());
            sessionContextService.addAssistantMessage(session, assistantContent);

            log.info("========== Agent编排完成(追问流程) ==========");
            return response;
        }

        // 分支3: 信息不足但追问已达上限,强制分诊
        if (infoInsufficient && !session.canAskClarification()) {
            log.info("[Orchestrator] 追问已达上限,强制进入分诊");
        }

注意这里的判断条件是两个都要看 :完整度阈值是硬性判断,AI的needsMoreInfo是软性判断。

条件 completeness < 0.6 isNeedClarification 结果
场景1 ✔ true ✗ false infoInsufficient = true → 进入追问
场景2 ✗ false (≥0.6) ✔ true infoInsufficient = true → 进入追问
场景3 ✗ false (≥0.6) ✗ false infoInsufficient = false → 不追问, 直接分诊
  • 即使 completeness ≥ 0.6,只要 AI 返回 isNeedClarification=true,且追问次数还没用完,就会继续追问。这意味着追问并不是固定3次,而是受两个因素共同控制:

    • AI 自身判断 --- isNeedClarification 是否为 true
    • 追问上限 --- session.canAskClarification() 是否还有余额
  • 举个例子:

    • 如果第1次 completeness 就达到 0.8 且 isNeedClarification=false,那就直接跳过追问,进入分诊,不会强制追问3次。
    • 反过来,如果 AI 每次都返回 isNeedClarification=true,那就会一直追问直到达到上限才强制分诊(分支3)。
  • 为什么要用COMPLETENESS_THRESHOLD = 0.6这个硬性阈值?又为什么要设置追问次数上限?接着看踩坑。

踩坑:AI几乎总是返回needsMoreInfo=true

这是个大坑。你问AI"信息够不够",它几乎永远说"不够,再问问吧"。因为AI天生倾向于想要更多信息------多问一句总比少问一句安全嘛。

但用户体验受不了。用户都说了"头疼、恶心、血压160/100",信息明明够了,AI还说"需要追问:请问头疼持续多久了?"这就过度了。

所以我加了COMPLETENESS_THRESHOLD = 0.6这个硬性阈值。AI说不够,但完整度已经到0.7了,那就直接分诊,不再追问。AI的建议是参考,不是圣旨。

追问技巧

追问不是随便问的,问得好用户愿意配合,问得不好用户直接关页面。几个实操技巧:

  1. 用选择题代替开放题。"头痛是持续性的还是间歇性的?"比"头痛是什么样的?"好回答多了。开放题用户不知道说什么,选择题点一下就行。

  2. 结合已知信息追问。用户提到"血压偏高",那就问"请问目前是否在服用降压药?"------这比干巴巴问"您在吃什么药?"有针对性得多。

  3. 关注时间维度。"症状是从什么时候开始的?持续了多久?"------时间信息对分诊特别重要,急性发作和慢性反复完全是两码事。

  4. 绝不诱导性提问。不能问"是不是心脏不舒服?",这会引导用户朝特定方向描述。应该问"除了胸痛,还有没有其他不舒服?",让用户自己说。

踩坑:分诊完成后clarificationRound没重置

这里先分享一下分诊提示词,这篇文章分诊我说的很少,具体业务到下一篇章再去更新吧(到时候3agent编排一起说更清晰)

java 复制代码
    @Bean
    public String triageAgentPrompt() {
        return """
            你是一名专业的医疗分诊助手。请根据用户描述的症状,返回结构化的分诊建议。

            【重要规则】
            1. 不要进行疾病诊断,只提供就诊方向建议
            2. 不要开具处方或推荐具体药物
            3. 不要提供药物剂量
            4. 不要使用"你可能得了某某病"这样的表述
            5. 只推荐科室,不推荐具体医生
            6. 你是在信息已充分的情况下被调用的,needClarification 必须为 false,clarificationQuestions 必须为空数组,不要追问

            【输出要求】
            请严格按照以下JSON格式返回,不要包含任何其他文字:

            {
              "needEmergency": false,
              "urgencyLevel": "OUTPATIENT",
              "primaryDepartment": "推荐的科室",
              "alternativeDepartments": ["备选科室1", "备选科室2"],
              "confidenceScore": 0.7,
              "needClarification": false,
              "clarificationQuestions": [],
              "reasoningSummary": "分析原因",
              "preparationAdvice": ["建议1", "建议2"],
              "warningSigns": ["警示症状1", "警示症状2"],
              "disclaimer": "本系统仅提供就诊方向参考,不构成医疗诊断。"
            }

            【urgencyLevel取值说明】
            - IMMEDIATE: 立即拨打120急救
            - URGENT: 建议急诊就诊
            - OUTPATIENT: 建议门诊就诊
            - OBSERVATION: 症状轻微,可自行观察

            请用中文回复。
            """;
    }

这个bug藏得很深。现象是:用户完成一次分诊后,再输入新症状,系统直接返回"追问已达上限",不给分诊结果。

排查后发现,clarificationRound在分诊完成后没有重置。第一轮分诊追问了3次,clarificationRound=3,然后分诊完成。用户输入新症状,进来一判断canAskClarification()返回false(因为3 >= 3),直接跳过追问走分诊------但这时候AI拿到的信息又不够,分诊结果置信度很低。

修复很简单,分诊完成后重置计数器:

java 复制代码
public void resetClarificationRound() {
    clarificationRound = 0;
}

在分诊完成的逻辑里调用一下就行。但这个bug让我意识到,会话状态管理比想象中容易出错。每个阶段切换的时候,都要想想哪些状态需要重置。

前端localStorage保存sessionId

  • 会话状态存在后端内存里,前端通过sessionId来关联,最终知道这是第几轮会话。
  • 问题是用户刷新页面后,sessionId就丢了,后端的会话数据还在,但前端找不到了。
  • 简单说: 没有sessionId,每次请求都是全新会话,多轮追问就崩了 。
场景 有sessionId 没有sessionId
刷新页面后 从localStorage恢复,继续同一轮对话 丢失会话,重新开始
追问轮次 后端知道是第几轮,不会超3次 每轮都是第1轮,无限追问
历史消息 后端有完整上下文 每次只有当前一条消息

解决方法是在前端用localStorage持久化:

javascript 复制代码
// 发起分诊请求
async function startTriage(symptom) {
    const sessionId = localStorage.getItem('triage_session_id');
    const response = await fetch('/api/v1/triage', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            message: symptom,
            sessionId: sessionId || null
        })
    });
    const result = await response.json();
    // 保存sessionId
    localStorage.setItem('triage_session_id', result.sessionId);
    return result;
}

然后在页面加载时再从localStorage 读取 triage_session_id :

javascript 复制代码
const currentSessionId = ref<string | null>(localStorage.getItem('triage_session_id'))

所以刷新后 currentSessionId 自动恢复为 'abc-123' ,下次 sendTriage 就会带上它。【但要注意一个边界情况: 用户结束一次问诊后,再次打开页面 ,localStorage里还留着旧的sessionId。这会导致新对话继续用旧会话,历史消息混在一起。这个后续优化一下,允许用户自己去结束问诊,然后清空localStorage】

typescript 复制代码
页面刷新前 ──→ localStorage.setItem('triage_session_id', 'abc-123')
     ↓ 刷新
页面加载 ──→ localStorage.getItem('triage_session_id') → 'abc-123'
     ↓
currentSessionId = 'abc-123'

后端的SessionContextService在收到请求时,先看有没有sessionId,有的话从内存Map里取会话,没有就创建新的:

java 复制代码
@Service
public class SessionContextService {
    // 内存存储,简单粗暴,后续可以换Redis
    private final Map<String, ConsultationSession> sessions = new ConcurrentHashMap<>();

    public ConsultationSession getOrCreateSession(String sessionId) {
        if (sessionId != null && sessions.containsKey(sessionId)) {
            return sessions.get(sessionId);
        }
        ConsultationSession newSession = ConsultationSession.builder()
                .sessionId(UUID.randomUUID().toString())
                .stage(ConsultationStage.INTAKE)
                .clarificationRound(0)
                .messages(new ArrayList<>())
                .build();
        sessions.put(newSession.getSessionId(), newSession);
        return newSession;
    }
}

ConcurrentHashMap是因为后续可能加多线程。当前单实例够用,真要上集群就得换Redis了,但那是部署层面的事,接口不用改。

前端还需要显示追问进度,让用户知道还能回答几轮:

javascript 复制代码
// 显示追问标签
if (result.needClarification) {
    showClarificationTag(`追问 ${result.clarificationRound}/3`);
    displayQuestions(result.clarificationQuestions);
}

五、验收和效果

写完这些逻辑,跑几个测试用例看看效果。

测试1:"胸口疼喘不过气" → 急症拦截

输入:"胸口疼喘不过气"

流程:用户输入 → EmergencyRuleService.check() → 命中组合规则"胸痛+呼吸困难" → 直接返回急症结果,不调AI

返回:

json 复制代码
{
  "needEmergency": true,
  "urgencyLevel": "IMMEDIATE",
  "primaryDepartment": "急诊科",
  "confidenceScore": 1.0,
  "reasoningSummary": "命中急症规则: 胸痛伴呼吸困难",
  "disclaimer": "本系统仅提供分诊建议,不构成医疗诊断。请以医生面诊结果为准。如有紧急情况,请立即拨打120。"
}

响应时间:2ms。如果走AI的话至少1-2秒。这就是规则引擎的价值。

测试2:只说"我最近头疼" → 追问→ 分诊→ 科室推荐

(1)第一轮:
  • 输入:

    {
    "message": "我最近头疼"
    }

  • 流程:用户输入 → EmergencyRuleService.check() → 未命中 → AI分析 → 信息完整度0.25 < 0.6 → 追问

  • 返回:

json 复制代码
{
    "needEmergency": false,
    "urgencyLevel": "OBSERVATION",
    "primaryDepartment": "待定",
    "alternativeDepartments": [],
    "confidenceScore": 0.25,
    "needClarification": true,
    "clarificationQuestions": [
        "头痛大概持续多长时间了?能具体说一下是几天还是几周吗?",
        "头痛的程度怎么样?是轻微隐痛还是比较剧烈?有没有影响您的日常活动或睡眠?"
    ],
    "reasoningSummary": "信息不足,需要进一步了解症状详情",
    "preparationAdvice": [],
    "warningSigns": [],
    "disclaimer": "本系统仅提供就诊方向参考,不构成医疗诊断。",
    "emergencyMessage": null,
    "errorMessage": null,
    "status": "success",
    "sessionId": "ff5fd677-65b1-4723-a323-6c3515d2a127",
    "stage": "CLARIFICATION",
    "clarificationRound": 1,
    "agentTrace": [
        {
            "agentName": "IntakeAgent",
            "inputSummary": "用户消息: 我最近头疼",
            "outputSummary": "完整度:0.25, 追问:true",
            "durationMs": 15199,
            "modelName": null,
            "success": true,
            "errorMessage": null
        }
    ]
}

前端展示追问标签"追问 1/3",用户回答后带着sessionId继续请求,后端从会话中恢复上下文,进入第2轮追问或直接分诊。

(2)第二轮:
  • 输入:

    {
    "sessionId": "ff5fd677-65b1-4723-a323-6c3515d2a127",
    "message": "下班后就开始头疼,2小时了\n轻微,没有影响到日常活动"
    }

  • 流程:用户输入 → EmergencyRuleService.check() → 未命中 → AI分析 → 信息完整度0.53 < 0.6 → 追问

  • 返回:

typescript 复制代码
{
    "needEmergency": false,
    "urgencyLevel": "OBSERVATION",
    "primaryDepartment": "待定",
    "alternativeDepartments": [],
    "confidenceScore": 0.525,
    "needClarification": true,
    "clarificationQuestions": [
        "头痛的时候有没有其他不舒服的感觉?比如恶心、呕吐、看东西模糊、怕光、发热等?",
        "请问您的年龄和性别是?"
    ],
    "reasoningSummary": "信息不足,需要进一步了解症状详情",
    "preparationAdvice": [],
    "warningSigns": [],
    "disclaimer": "本系统仅提供就诊方向参考,不构成医疗诊断。",
    "emergencyMessage": null,
    "errorMessage": null,
    "status": "success",
    "sessionId": "ff5fd677-65b1-4723-a323-6c3515d2a127",
    "stage": "CLARIFICATION",
    "clarificationRound": 2,
    "agentTrace": [
        {
            "agentName": "IntakeAgent",
            "inputSummary": "用户消息: 下班后就开始头疼,2小时了\n轻微,没有影响到日常活动",
            "outputSummary": "完整度:0.53, 追问:true",
            "durationMs": 19244,
            "modelName": null,
            "success": true,
            "errorMessage": null
        }
    ]
}
(3)第三轮:
  • 输入:
json 复制代码
{
    "sessionId": "ff5fd677-65b1-4723-a323-6c3515d2a127",
    "message": "看东西模糊、26、男"
}
  • 流程:用户输入 → EmergencyRuleService.check() → 未命中 → AI分析 → 信息完整度0.78 > 0.6 → 分诊并返回报告

  • 返回:

json 复制代码
{
    "needEmergency": false,
    "urgencyLevel": "OUTPATIENT",
    "primaryDepartment": "眼科",
    "alternativeDepartments": [
        "神经内科",
        "普通内科"
    ],
    "confidenceScore": 0.78,
    "needClarification": false,
    "clarificationQuestions": [],
    "reasoningSummary": "患者为26岁男性,出现轻度头痛伴视物模糊,症状持续约2小时且未影响日常活动。头痛轻微且无恶心、呕吐、发热等急诊危象,首要考虑眼部原因(如屈光不正、眼疲劳或眼部血管问题),因此建议先挂眼科;如眼科检查未见异常,可进一步转至神经内科或普通内科评估。",
    "preparationAdvice": [
        "携带本人身份证件及近期体检报告(如有)",
        "记录头痛出现的时间、持续时长、加重或缓解因素",
        "避免长时间使用电子屏幕,保持适度休息",
        "携带最近一次眼科检查报告或配镜记录",
        "准备好当前使用的眼药或口服药物清单",
        "建议有亲友陪同并提前规划出行路线"
    ],
    "warningSigns": [
        "突发剧烈头痛或"雷击样"疼痛",
        "伴随呕吐、意识改变、肢体无力或麻木",
        "视力急剧下降或出现视野缺损",
        "突发视力明显下降或失明",
        "眼球剧痛伴红、光感异常"
    ],
    "disclaimer": "本系统仅提供就诊方向参考,不构成医疗诊断。",
    "emergencyMessage": null,
    "errorMessage": null,
    "status": "success",
    "sessionId": "ff5fd677-65b1-4723-a323-6c3515d2a127",
    "stage": "TRIAGE",
    "clarificationRound": 2,
    "agentTrace": [
        {
            "agentName": "IntakeAgent",
            "inputSummary": "用户消息: 看东西模糊\n26、男",
            "outputSummary": "完整度:0.90, 追问:false",
            "durationMs": 17560,
            "modelName": null,
            "success": true,
            "errorMessage": null
        },
        {
            "agentName": "TriageAgent",
            "inputSummary": "症状摘要: 26岁男性,下班后出现头痛,持续约2小时,程度轻微,不影响日常活动,伴有视物模糊。",
            "outputSummary": "科室:眼科, 紧急:OUTPATIENT, 置信度:0.78",
            "durationMs": 5531,
            "modelName": null,
            "success": true,
            "errorMessage": null
        },
        {
            "agentName": "ReportAgent",
            "inputSummary": "分诊报告, 科室: 眼科",
            "outputSummary": "报告生成完成",
            "durationMs": 6937,
            "modelName": null,
            "success": true,
            "errorMessage": null
        }
    ]
}

小结

这一篇解决了三个核心问题:

  1. AI输出不可控 → 结构化JSON + 容错解析器,AI再怎么折腾都能兜住
  2. 急症响应慢 → 规则引擎前置,命中急症关键词直接返回,不走AI
  3. AI乱说话 → 安全过滤 + Prompt约束 + 免责声明,三重保险
  4. 信息不足硬分诊 → 多轮追问 + 完整度阈值,信息不够就问,问够了再分诊

踩的坑也不少:AI返回的JSON格式千奇百怪、AI总说信息不够、追问计数忘了重置、AI会绕过禁词......这些都是真实开发中会遇到的问题,不是写个Demo就能发现的。

ps:因为我的代码已经拆分到3agent了,本篇就不写单agent的具体代码了,了解大致业务即可,后续多agent与分诊等详细代码请看下一篇

下一篇要把现在挤在一起的逻辑拆成三个Agent------IntakeAgent负责采集症状、TriageAgent负责分诊、ReportAgent负责生成报告------然后用一个编排器来协调它们。这就是三Agent编排,也是整个项目从"能跑"到"能维护"的关键一步。


相关推荐
葫芦和十三14 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
考虑考虑15 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯16 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
带刺的坐椅17 小时前
从 Claude Code 隐私争议,看 SolonCode 的设计选择
ai·llm·agent·claudecode·soloncode·codingplan
青石路20 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
lincats21 小时前
Claude Code项目越写越乱?这套清理流程能救你
ai·ai agent·claude code
后端小肥肠21 小时前
小红书虚拟商品怎么做?我先用 Skill 跑通了壁纸品类
人工智能·aigc·agent
Java陈序员1 天前
企业级!一个基于 Java 开发的开源 AI 应用开发平台!
spring boot·agent·mcp
Chen666781 天前
我让一个Agent Team长时间自治运行后,发现问题不在“怎么组队”
agent
Randyliu1 天前
20260508-Agent搭建记录以及对ReAct框架的理解
面试·agent