AI助手临床校验改为只提示不拦截,资料不全也给出解答

- TcmClinicalReportStream 新增 BLOCKING 开关(默认 false),校验命中时照常交付原文,问题码记入 warnings()
- 问答、长报告、结构化分析、报告生成的脉象/事实状态/溯源校验改为随结果返回 clinical_review_warnings
- 处方草稿丢弃多余脉象字段、重复药名保留首味,不再整张失败
- 接诊提示词补充互联网面诊仅舌象无脉象、须合理推断给出结论

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
gr
2026-10-27 15:00:10 +08:00
co-authored by Claude Opus 5.5
parent f9cbd42113
commit ea40cfdfed
3 changed files with 102 additions and 29 deletions
@@ -7,10 +7,17 @@ namespace app\adminapi\service;
/** Validate complete prose units before exposing their original bytes to clients. */
final class TcmClinicalReportStream
{
/**
* 互联网面诊只有舌象等部分资料,校验规则命中时不再截断回答:
* 原文照常交付,问题码记入 warnings() 供医师复核。设为 true 恢复拦截模式。
*/
public const BLOCKING = false;
private string $pending = '';
private string $accepted = '';
private bool $stopped = false;
private array $issues = [];
private array $warnings = [];
private \Closure $forward;
private ?\Closure $onReject;
private string $deferred = '';
@@ -69,10 +76,11 @@ final class TcmClinicalReportStream
if ($this->finalCheckpointQuery === null) return true;
$candidate = $this->accepted . $this->deferred;
$review = $this->review($candidate, true);
if (empty($review['valid'])) return $this->reject($this->deferred, $review['issues']);
if (empty($review['valid']) && !$this->reject($this->deferred, $review['issues'])) return false;
$this->finalContract = TcmLongReportContract::validate($candidate, $this->finalCheckpointQuery);
if (empty($this->finalContract['valid'])) {
return $this->reject($this->deferred, [['code' => 'REPORT_FINAL_CONTRACT_INVALID', 'source_id' => 'report']]);
if (empty($this->finalContract['valid'])
&& !$this->reject($this->deferred, [['code' => 'REPORT_FINAL_CONTRACT_INVALID', 'source_id' => 'report']])) {
return false;
}
if ($this->shouldAbort !== null && ($this->shouldAbort)()) { $this->stopped = true; return false; }
if ($this->deferred === '') return true;
@@ -86,14 +94,16 @@ final class TcmClinicalReportStream
public function rejected(): bool { return $this->issues !== []; }
public function issues(): array { return $this->issues; }
public function warnings(): array { return $this->warnings; }
public function acceptedContent(): string { return $this->accepted; }
public function finalContract(): ?array { return $this->finalContract; }
private function release(string $unit): bool
{
if ($unit === '') return true;
if (in_array(trim($unit), $this->priorAnswerUnits, true) && !$this->isOrdinaryHeadingUnit($unit)) {
return $this->reject($unit, [['code' => 'REPORT_CONTINUATION_REPEATED_SECTION', 'source_id' => 'report']]);
if (in_array(trim($unit), $this->priorAnswerUnits, true) && !$this->isOrdinaryHeadingUnit($unit)
&& !$this->reject($unit, [['code' => 'REPORT_CONTINUATION_REPEATED_SECTION', 'source_id' => 'report']])) {
return false;
}
if ($this->finalCheckpointQuery !== null && !$this->holdingFinal) {
$offset = 0;
@@ -115,7 +125,7 @@ final class TcmClinicalReportStream
}
$candidate = $this->accepted . $unit;
$review = $this->review($candidate);
if (empty($review['valid'])) return $this->reject($unit, $review['issues']);
if (empty($review['valid']) && !$this->reject($unit, $review['issues'])) return false;
if (($this->forward)($unit) === false) {
$this->stopped = true;
return false;
@@ -158,6 +168,15 @@ final class TcmClinicalReportStream
private function reject(string $unit, array $issues): bool
{
if (!self::BLOCKING) {
// 非拦截模式:只记录问题码,调用方继续按原流程交付该段原文。
foreach ($issues as $issue) {
if (!is_array($issue)) continue;
$key = ($issue['code'] ?? '') . '|' . ($issue['source_id'] ?? '');
if (!isset($this->warnings[$key]) && count($this->warnings) < 32) $this->warnings[$key] = $issue;
}
return true;
}
if ($this->holdingFinal) {
$issues[] = ['code' => 'REPORT_FINAL_SECTION_UNDELIVERED', 'source_id' => 'report'];
}