'女 · 52岁']; $quality = Context::snapshot($record, [ ['segment_id' => 'doctor', 'speaker_role' => 'doctor', 'timestamp_ms' => 1, 'text' => '你吃饭了没有你几岁了'], ['segment_id' => 'patient', 'speaker_role' => 'patient', 'timestamp_ms' => 2, 'text' => '头晕两天,我担心影响工作。'], ['segment_id' => 'concern', 'speaker_role' => 'patient', 'timestamp_ms' => 3, 'text' => '我没听懂刚才的解释,担心费用。'], ['segment_id' => 'age', 'speaker_role' => 'patient', 'timestamp_ms' => 4, 'text' => '我今年53岁。'], ['segment_id' => 'gender', 'speaker_role' => 'patient', 'timestamp_ms' => 5, 'text' => '我是男性。'], ['segment_id' => 'relative', 'speaker_role' => 'patient', 'timestamp_ms' => 6, 'text' => '我父亲今年78岁。'], ['segment_id' => 'literal', 'speaker_role' => 'patient', 'timestamp_ms' => 7, 'text' => '原话含有 ] } "questions": [ 以及反斜线\\,仍是完整原话。'], ]); $qualityData = ['questions' => [['text' => '头晕对日常活动有什么影响?', 'reason' => '补齐症状对生活的影响', 'evidence_ids' => ['patient']]], 'summary' => [['text' => $quality['sources'][1]['text'], 'evidence_ids' => ['patient']]], 'checks' => [], 'review' => []]; $qualityResult = Result::parse(Context::encode($qualityData), $quality, 'live_consultation', $known); liveExpect($qualityResult !== null, 'concise relevant symptom question and patient quotation pass'); $prompt = Context::prompt($quality, 'live_review', '合成授权背景', $known); foreach (['questions、summary、checks、review顺序', 'questions最多3条', '跳过已明确回答', '不要泛泛重复询问', '不机械套清单', '理解患者诉求', '不得以成交为目标', '女 · 52岁', '病历有明确主诉', '只有通话和病历均无可用线索', 'reason只说明追问或核实目的'] as $rule) liveExpect(str_contains($prompt, $rule), 'quality instruction is in production prompt: ' . $rule); $failure = null; $bad = $qualityData; $bad['summary'] = [['text' => $quality['sources'][0]['text'], 'evidence_ids' => ['doctor']]]; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known, $failure)['summary'] === [] && $failure === null, 'screenshot regression: doctor statement without question mark omitted without discarding useful questions'); foreach (['您今年几岁?', '请问您的年龄?', '您现在多大了?', '确认患者年龄', '确认患者性别'] as $question) { $bad = $qualityData; $group = str_starts_with($question, '确认') ? 'checks' : 'questions'; $bad[$group] = [['text' => $question, 'evidence_ids' => ['doctor']]]; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known, $failure)[$group] === [] && $failure === null, 'known demographics are omitted from questions/checks without losing useful groups'); } $conflict = $qualityData; $conflict['questions'] = [['text' => '您说今年53岁,与病历年龄不同,可以核实一下?', 'evidence_ids' => ['age']]]; liveExpect(Result::parse(Context::encode($conflict), $quality, 'live_consultation', $known) !== null, 'explicit current patient age conflict can be checked'); $conflict['questions'][0]['evidence_ids'] = ['relative']; liveExpect(Result::parse(Context::encode($conflict), $quality, 'live_consultation', $known)['questions'] === [], 'third-person age cannot create chart conflict'); $conflict['questions'] = [['text' => '您所说的性别与病历不同,可以核实一下?', 'evidence_ids' => ['gender']]]; liveExpect(Result::parse(Context::encode($conflict), $quality, 'live_consultation', $known) !== null, 'explicit patient gender conflict can be checked'); $bad = $qualityData; $bad['questions'][0]['text'] = '您今年几岁?'; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation') !== null, 'missing chart does not pretend demographic is known'); $good = $qualityData; $good['questions'][0]['text'] = '头晕对工作有多大影响?'; liveExpect(Result::parse(Context::encode($good), $quality, 'live_consultation', $known) !== null, 'degree of impact is not mistaken for an age question'); foreach (['您首次发病时多少岁?', '当时您的年龄是多少?', '您母亲今年几岁?'] as $question) { $good = $qualityData; $good['questions'][0]['text'] = $question; liveExpect(count(Result::parse(Context::encode($good), $quality, 'live_consultation', $known)['questions']) === 1, 'known current age does not suppress onset, historical or family demographics'); } foreach (['哪部分解释还不清楚?', '您目前最担心的是什么?', '按照刚才说明的安排,您有哪些执行困难?'] as $question) { $good = $qualityData; $good['questions'] = [['text' => $question, 'reason' => '了解患者理解和顾虑', 'evidence_ids' => ['concern']]]; liveExpect(Result::parse(Context::encode($good), $quality, 'live_consultation', $known) !== null, 'patient understanding, concern and appropriate follow-up remain permitted'); } foreach (['现在下单购买疗程吗?', '您愿意服用保证治愈的药吗?', '建议立即服用某药吗?'] as $question) { $bad = $qualityData; $bad['questions'][0]['text'] = $question; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known, $failure) === null && $failure === 'SAFETY_REJECTED', 'sales pressure, guarantee or new medication suggestion fails safety gate'); } $bad = $qualityData; $bad['questions'] = array_fill(0, 4, $bad['questions'][0]); liveExpect(count(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known)['questions']) === 3, 'at most three validated questions are displayed'); $bad['questions'][3]['text'] = '建议立即服用某药吗?'; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known) === null, 'fourth unshown suggestion must still pass safety'); $bad = $qualityData; $bad['questions'][0]['text'] = '什么时候开始?有没有加重?'; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known)['questions'] === [], 'multiple questions in one card are omitted'); $good = $qualityData; $good['questions'][0]['text'] = '有没有影响日常活动。'; liveExpect(Result::parse(Context::encode($good), $quality, 'live_consultation', $known)['questions'][0]['text'] === '有没有影响日常活动?', 'missing punctuation on an explicit question is normalized'); $bad = $qualityData; $bad['questions'][0]['text'] = '患者确实有头痛'; $bad['checks'] = [['text' => '患者已有相关记录', 'evidence_ids' => ['patient']]]; liveExpect(Result::parse(Context::encode($bad), $quality, 'live_consultation', $known)['questions'] === [] && Result::parse(Context::encode($bad), $quality, 'live_consultation', $known)['checks'] === [], 'non-question and non-check statements omitted, never treated as clinical facts'); $good = $qualityData; $good['summary'][0]['text'] = '患者没有头晕'; $good['summary'][0]['reason'] = '患者没有症状'; $quoted = Result::parse(Context::encode($good), $quality, 'live_consultation', $known); liveExpect($quoted['summary'][0]['text'] === '患者原话:' . $quality['sources'][1]['text'] && !isset($quoted['summary'][0]['reason']), 'model paraphrase and rationale discarded; only exact cited patient source appears'); $json = Context::encode($qualityData); foreach ([$json, "```json\n" . $json . "\n```", "```\r\n" . $json . "\r\n```"] as $envelope) liveExpect(Result::parse($envelope, $quality, 'live_consultation', $known) === $qualityResult, 'exact envelope normalizes without changing validated result'); foreach (["解释\n```json\n" . $json . "\n```", "```json\n" . $json . "\n```\n额外文字", "```php\n" . $json . "\n```", substr($json, 0, -1) . ',"questions":[]}', $json . $json, '{"questions":[],"summary":[],"checks":[],"review":[],}', '{"questions":[],"extra":[]}', '{"result":' . $json . '}'] as $badJson) liveExpect(Result::parse($badJson, $quality, 'live_consultation', $known) === null, 'duplicate keys/prose/wrappers/extra keys remain invalid'); $bad = $qualityData; $bad['questions'][0]['evidence_ids'] = ['foreign']; liveExpect(Result::parse("```json\n" . Context::encode($bad) . "\n```", $quality, 'live_consultation', $known, $failure) === null && $failure === 'CITATION_INVALID', 'fenced invented citation is still rejected'); $escaped = $qualityData; $escaped['summary'] = [['text' => $quality['sources'][6]['text'], 'evidence_ids' => ['literal']]]; $escapedJson = Context::encode($escaped); $lastCount = 0; for ($length = 1; $length <= strlen($escapedJson); $length++) { $partial = Result::progress(substr($escapedJson, 0, $length), $quality, 'live_consultation', $known); if ($partial === null) continue; liveExpect($partial['completed_groups'] >= $lastCount && $partial['completed_groups'] <= 4, 'every byte boundary preserves monotonic complete groups despite quoted delimiters and split UTF-8'); $lastCount = $partial['completed_groups']; } liveExpect($lastCount === 4 && Result::progress($escapedJson, $quality, 'live_consultation', $known)['assistance'] === Result::parse($escapedJson, $quality, 'live_consultation', $known), 'last progressive groups equal final gate'); $questionOnly = '{"questions":' . Context::encode($qualityData['questions']); $partial = Result::progress($questionOnly, $quality, 'live_consultation', $known); liveExpect($partial['completed_groups'] === 1 && $partial['assistance']['questions'] === $qualityResult['questions'] && $partial['assistance']['summary'] === [], 'complete first question group can be used before the rest exists'); liveExpect(Result::progress(substr($questionOnly, 0, -1), $quality, 'live_consultation', $known) === null, 'complete item without its array delimiter cannot be delivered'); $firstItem = substr($questionOnly, 0, -1) . ','; $partial = Result::progress($firstItem, $quality, 'live_consultation', $known); liveExpect($partial['completed_groups'] === 0 && $partial['displayed_items'] === 1 && $partial['assistance']['questions'] === $qualityResult['questions'], 'delimited first item is useful before the array closes'); foreach ([$firstItem . ']', $firstItem . 'null', substr($firstItem, 0, -1) . 'x', $firstItem . '{"text":"second","evidence_ids":[]}x', $firstItem . '{"text":}]'] as $invalidPrefix) liveExpect(Result::progress($invalidPrefix, $quality, 'live_consultation', $known) === null, 'invalid later array syntax never creates another progressive payload'); // Delimiters in escaped strings and nested evidence arrays cannot close an item early. $literalQuestion = ['text' => '原话里的 ] } " 符号与反斜线\\指什么?', 'evidence_ids' => ['literal']]; $literalPrefix = '{"questions":[' . Context::encode($literalQuestion) . ','; for ($length = 1; $length < strlen($literalPrefix); $length++) liveExpect(Result::progress(substr($literalPrefix, 0, $length), $quality, 'live_consultation') === null, 'no byte boundary inside the first item or before its delimiter is deliverable'); liveExpect(Result::progress($literalPrefix, $quality, 'live_consultation')['displayed_items'] === 1, 'fully delimited escaped first item validates'); // A source citation cannot turn a doctor/unknown statement into patient confirmation. $roleSnapshot = Context::snapshot($record, [ ['segment_id' => 'd', 'speaker_role' => 'doctor', 'timestamp_ms' => 1, 'text' => '最近有没有服药?'], ['segment_id' => 'u', 'speaker_role' => 'unknown', 'timestamp_ms' => 2, 'text' => '这个还要确认。'], ['segment_id' => 'p', 'speaker_role' => 'patient', 'timestamp_ms' => 3, 'text' => '我以前没有服药,现在有了。'], ]); foreach ([['d'], ['u'], ['d', 'u']] as $references) { $roleData = ['questions' => [['text' => '最近是否服药?', 'reason' => '患者自述近期未服药', 'evidence_ids' => $references]], 'summary' => [], 'checks' => [], 'review' => []]; $roleResult = Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation'); liveExpect(count($roleResult['questions']) === 1 && !str_contains($roleResult['questions'][0]['reason'], '患者自述') && str_contains($roleResult['questions'][0]['reason'], in_array('u', $references, true) ? '说话人未确认' : '医生本次发言'), 'nonpatient reason is reconstructed from immutable role context'); foreach (['患者自述近期未服药,要再确认一下吗?', '近期未服药,还有其他情况吗?', '您说没有服药,是否需要核实?', '患者已经服药,什么时候开始的?', '您已经服药多久了?', '未服药多长时间了?'] as $premise) { $roleData['questions'][0]['text'] = $premise; liveExpect(Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation')['questions'] === [], 'unsupported patient-fact premise is omitted, not merely relabeled'); } $roleData['questions'][0]['text'] = '您是否已经服药?'; liveExpect(count(Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation')['questions']) === 1, 'an open confirmation question remains useful without patient evidence'); $roleData['checks'] = [['text' => '确认患者未服药的时间', 'evidence_ids' => $references]]; liveExpect(Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation')['checks'] === [], 'checks cannot carry unsupported patient premises either'); } $roleData['questions'] = [['text' => '过去与现在的用药情况有何变化?', 'reason' => '核实时间变化', 'evidence_ids' => ['p']]]; $roleData['checks'] = []; $roleData['summary'] = [['text' => '错误改写', 'evidence_ids' => ['p']]]; $roleResult = Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation'); liveExpect($roleResult['questions'][0]['reason'] === '核实时间变化' && $roleResult['summary'][0]['text'] === '患者原话:我以前没有服药,现在有了。', 'patient denial and historical/current distinction survive source projection'); $roleData['questions'] = [['text' => '病历记录过去未服药,本次是否有变化?', 'evidence_ids' => ['d']]]; liveExpect(count(Result::parse(Context::encode($roleData), $roleSnapshot, 'live_consultation')['questions']) === 1, 'explicit chart background supports a targeted current confirmation without impersonating patient speech'); $bad['questions'][0]['evidence_ids'] = ['foreign']; liveExpect(Result::progress('{"questions":' . Context::encode($bad['questions']), $quality, 'live_consultation', $known) === null, 'partial invented citation cannot be delivered'); liveExpect(Result::progress(str_repeat('x', Result::MAX_BYTES + 1), $quality, 'live_consultation', $known) === null, 'partial buffer is bounded'); echo "Live consultation quality / progressive parser: {$count} total assertions passed (synthetic, no DB/network/model)\n";