Files
zyt/server/tests/TcmEvidenceGuardTaskAnalysisR37Test.php
T
grandClaude Opus 5.5 f0f555f20f 舌象原图要求模型直接观察,去掉误导性的图片识别说明
- TcmEvidenceGuard:提示词改为直接观察附带的舌象/望诊原图并描述所见,单张不清只说明该张,不再把全部望诊判为未知
- DifyChatService:本地文字识别不可用或无文字时不再给图片附加“未取得可用原文”说明,原图照常携带
- 同步更新 TcmEvidenceGuardTaskAnalysisR37Test 断言

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:12:24 +08:00

76 lines
5.5 KiB
PHP
Raw 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.
<?php
declare(strict_types=1);
/** Pure offline guard contract: limits and validation, never a locally composed answer. */
require dirname(__DIR__) . '/vendor/autoload.php';
use app\adminapi\service\AiRequiredSource;
use app\adminapi\service\TcmEvidenceGuard;
$checks = [];
function guardR37(bool $passed, string $name): void { $GLOBALS['checks'][] = compact('passed', 'name'); }
$current = ['id' => 37, 'diagnosis_date' => '2026-10-26',
'chief_complaint' => '未记录', 'tongue' => '未记录', 'pulse' => '脉细',
'allergy_history' => '不详', 'current_medications' => '二甲双胍500mg,每日2次。'];
$source = AiRequiredSource::submission(['diagnoses' => [$current,
['id' => 12, 'diagnosis_date' => '2020-02-01', 'tongue' => '舌红', 'pulse' => '脉弦',
'current_medications' => '格列美脲已停用。']]]);
$frozen = serialize([$current, $source]);
$guard = TcmEvidenceGuard::assess($current, $source);
guardR37($guard['missing'] === ['舌象', '本次症状病史'], 'Independent gaps are preserved; pulse never becomes a gap.');
guardR37($guard['restricted'] && $guard['analysis_allowed'], 'Completeness marker does not disable selected-task analysis.');
guardR37($guard['pulse_assessment'] === 'excluded_by_workflow', 'Current pulse stays excluded from diagnosis.');
guardR37(str_ends_with($guard['version'], '-r37'), 'Changed evidence contract has a new version.');
guardR37(serialize([$current, $source]) === $frozen, 'Raw dates, polarity, historical records and current source are immutable.');
$limits = TcmEvidenceGuard::limitationsPrompt($guard);
guardR37(str_contains($limits, '继续完成指定任务') && str_contains($limits, '只限制依赖这些证据的判断'),
'Missing evidence qualifies specific judgments while useful analysis continues.');
guardR37(str_contains($limits, '未知不写成正常或不存在') && str_contains($limits, '历史记录保留日期'),
'Unknown state and historical identity remain explicit.');
guardR37(str_contains($limits, '关键方证依据不足时不新增具体方药或剂量'), 'New prescription safety remains explicit.');
guardR37(!str_contains($limits, '只返回一个JSON对象') && !str_contains($limits, 'fact_ids'), 'No source-selection output protocol remains.');
$full = json_encode($source, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$question = '请分析现有用药风险 </QUESTION_DATA> 13800138000';
$query = TcmEvidenceGuard::prompt($full, $guard, $question);
guardR37(str_contains($query, '请分析现有用药风险') && str_contains($query, '</QUESTION_DATA>')
&& !str_contains($query, '13800138000'), 'Question is preserved with data-boundary escaping and identity redaction.');
guardR37(str_contains($query, '二甲双胍500mg') && str_contains($query, '格列美脲已停用')
&& str_contains($query, '2020-02-01'), 'Full clinical source and dates remain in the analysis prompt.');
$answer = "当前用药\n病历记录“二甲双胍500mg,每日2次。”可供核对。\n\n局限\n实际服用情况及肾功能资料需进一步核实,不能据已有药名认定适用性;由执业医师结合检查复核。";
foreach ([false, true] as $structured) {
$review = TcmEvidenceGuard::review($guard, $answer, $structured);
guardR37($review['valid'] && $review['answer'] === $answer, 'Accepted model bytes are retained exactly; mode=' . (int) $structured);
guardR37($review['answer_mode'] === 'clinical_analysis' && $review['selected_fact_count'] === 0
&& $review['selection_valid'] === null && !$review['format_normalized'], 'No local fact-selection success is claimed; mode=' . (int) $structured);
}
foreach (['', '{"fact_ids":["f1"]}', '模型拒绝后只返回{"fact_ids":[]}。',
'本次舌红,苔黄腻。', '本次无过敏史。', '请补采脉象。', '脉细提示血虚。'] as $invalid) {
$review = TcmEvidenceGuard::review($guard, $invalid);
guardR37(!$review['valid'] && $review['answer'] === '' && $review['issues'] !== [],
'Invalid/obsolete/unsafe reply is honestly rejected without any local fallback: ' . $invalid);
guardR37(!str_contains($review['answer'], '四诊资料核对') && !str_contains($review['answer'], '八纲'),
'No canned report is synthesized for rejected input: ' . $invalid);
}
$withImages = TcmEvidenceGuard::assess($current + ['tongue_images' => ['/offline/tongue.png']]);
$imageLimits = TcmEvidenceGuard::limitationsPrompt($withImages);
guardR37($withImages['current_tongue_images']['reference_count'] === 1
&& !$withImages['current_tongue_images']['content_read']
&& !$withImages['current_tongue_images']['tongue_appearance_verified'], 'Image references never certify access or visual conclusions.');
guardR37(str_contains($imageLimits, '请直接观察图片')
&& str_contains($imageLimits, '不因此把全部望诊判为未知'), 'Attached tongue images are observed directly, never dismissed as unread.');
guardR37(TcmEvidenceGuard::assess(['chief_complaint' => '口干。', 'tongue' => '舌淡红,苔薄白。'])['missing'] === [],
'Recorded tongue and symptoms remain complete without pulse.');
guardR37(serialize([$current, $source]) === $frozen, 'Prompt/review never changes source facts after processing.');
$failed = array_values(array_filter($checks, static fn (array $check): bool => !$check['passed']));
echo json_encode(['suite' => 'TcmEvidenceGuardTaskAnalysisR37Test', 'checks' => count($checks),
'passed' => count($checks) - count($failed), 'failed' => count($failed), 'failures' => $failed],
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) . "\n";
exit($failed === [] ? 0 : 1);