diff --git a/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php b/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php index a5cc76367..faf8c2461 100644 --- a/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php +++ b/server/app/adminapi/logic/tcm/DiagnosisAiLogic.php @@ -25,7 +25,7 @@ class DiagnosisAiLogic extends BaseLogic private const ASSISTANT_PROMPT_VERSION = 'patient-context-assistant-nihaixia-v3'; - private const ANALYSIS_PROMPT_VERSION = 'patient-context-analysis-nihaixia-v3'; + private const ANALYSIS_PROMPT_VERSION = 'patient-context-analysis-nihaixia-v4'; private const MAX_ASSISTANT_PROMPT_LENGTH = 500; @@ -791,35 +791,39 @@ class DiagnosisAiLogic extends BaseLogic return null; } - try { - $result = DifyChatService::chat( - $profile, - self::buildUpstreamInputs( - $context, - '诊单结构化分析', - self::ANALYSIS_PROMPT_VERSION - ), - self::buildAnalysisPrompt($context), - 'admin-diagnosis-analysis-' . $adminId, - is_array($context['files'] ?? null) ? $context['files'] : [] - ); - } catch (\Throwable $e) { - Log::warning('diagnosis ai analysis upstream call failed', [ - 'diagnosis_id' => $diagnosisId, - 'profile' => $profile, - 'admin_id' => $adminId, - 'exception_class' => get_class($e), - ]); - self::setError('AI智能分析暂时不可用,请稍后重试'); - return null; - } + $inputs = self::buildUpstreamInputs($context, '诊单结构化分析', self::ANALYSIS_PROMPT_VERSION); + $prompt = self::buildAnalysisPrompt($context); + $files = is_array($context['files'] ?? null) ? $context['files'] : []; + $analysis = null; + for ($attempt = 0; $attempt < 2; $attempt++) { + try { + $result = DifyChatService::chat( + $profile, + $inputs, + $attempt === 0 ? $prompt : self::buildAnalysisRepairPrompt($prompt), + 'admin-diagnosis-analysis-' . $adminId, + $files + ); + } catch (\Throwable $e) { + Log::warning('diagnosis ai analysis upstream call failed', [ + 'diagnosis_id' => $diagnosisId, + 'profile' => $profile, + 'admin_id' => $adminId, + 'exception_class' => get_class($e), + ]); + self::setError('AI智能分析暂时不可用,请稍后重试'); + return null; + } - if (empty($result['ok'])) { - self::setError('AI智能分析暂时不可用,请稍后重试'); - return null; + if (empty($result['ok'])) { + self::setError('AI智能分析暂时不可用,请稍后重试'); + return null; + } + $analysis = self::parseAnalysisResponse((string) ($result['content'] ?? '')); + if ($analysis !== null) { + break; + } } - - $analysis = self::parseAnalysisResponse((string) ($result['content'] ?? '')); if ($analysis === null) { self::setError('AI返回的分析结构不符合要求,请重试'); return null; @@ -832,7 +836,7 @@ class DiagnosisAiLogic extends BaseLogic 'model_name' => $modelName, 'generated_at' => date('Y-m-d H:i:s'), 'context_scope' => (string) ($context['context_scope'] ?? 'patient_longitudinal'), - 'context_version' => (string) ($context['context_version'] ?? self::ANALYSIS_PROMPT_VERSION), + 'context_version' => self::ANALYSIS_PROMPT_VERSION, 'source_summary' => is_array($context['source_summary'] ?? null) ? $context['source_summary'] : [], @@ -1450,6 +1454,13 @@ PROMPT; PROMPT; } + private static function buildAnalysisRepairPrompt(string $originalPrompt): string + { + return $originalPrompt . "\n\n格式校验提醒:请重新独立作答,只输出一个可解析的 JSON 对象,且仅有 diagnosis_advice、risk_assessment、treatment_advice 三个键。" + . '前后不要解释、代码块或参考资料版本标识;两个建议字段各不超过 500 字;risk_assessment 最多 5 项,' + . '每项仅有 label(不超过 80 字)与小写 level(high、medium、low)。资料不足时说明缺口,不能编造风险或处方。'; + } + /** * 让患者纵向资料适配单次上游请求的体积上限。 * diff --git a/server/tests/DiagnosisAiAnalysisContractTest.php b/server/tests/DiagnosisAiAnalysisContractTest.php index 5e7cdbc8d..2c2cd3e05 100644 --- a/server/tests/DiagnosisAiAnalysisContractTest.php +++ b/server/tests/DiagnosisAiAnalysisContractTest.php @@ -50,7 +50,19 @@ $analysisSource = implode('', array_slice( )); analysisContractExpect( substr_count($analysisSource, 'DifyChatService::chat(') === 1, - 'one analysis request performs exactly one upstream chat call' + 'analysis retries malformed output through the same bounded upstream call site' +); +analysisContractExpect( + str_contains($analysisSource, '$attempt < 2') + && str_contains($analysisSource, 'buildAnalysisRepairPrompt($prompt)'), + 'malformed structured output receives at most one format repair attempt' +); +$repair = $logicReflection->getMethod('buildAnalysisRepairPrompt')->invoke(null, 'ORIGINAL_CASE_PROMPT'); +analysisContractExpect( + str_contains($repair, 'ORIGINAL_CASE_PROMPT') + && str_contains($repair, '仅有 diagnosis_advice、risk_assessment、treatment_advice 三个键') + && str_contains($repair, '资料不足时说明缺口'), + 'repair keeps the original patient evidence and constrained schema' ); foreach ([ "'diagnosis_advice'",