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编排,也是整个项目从"能跑"到"能维护"的关键一步。


相关推荐
俊哥V1 小时前
AI一周事件 · 2026-06-03 至 2026-06-09
人工智能·ai
金融RPA机器人丨实在智能1 小时前
橡胶原料供应链转型:海外AI Agent适配国产进销存系统改造费用解析与实在Agent降本方案
人工智能·ai
共创splendid--与您携手2 小时前
AI读取前端项目生成skill.md
前端·人工智能·ai
San813_LDD3 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript
猿小猴子4 小时前
主流 AI IDE 之一的「DeepSeek-Reasonix 」介绍
人工智能·ai·deepseek·reasonix
装不满的克莱因瓶4 小时前
链式法则如何传递参数误差 —— 深入理解神经网络中的梯度传播
人工智能·python·深度学习·神经网络·数学·机器学习·ai
Anastasiozzzz4 小时前
从有限状态机到智能体图:传统 FSM 与 Agent Graph的演进
java·人工智能·python·ai
唐某人丶10 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程