From 55622368e3bf6228a847329a50778703974f8dcb Mon Sep 17 00:00:00 2001 From: admin <572701190@qq.com> Date: Sat, 2 May 2026 04:39:18 +0800 Subject: [PATCH] Prioritize doctor instructions in AI report prompts - Extract AI prompt construction into a tested utility for report editor generation. - Make doctor instructions and structured report fields higher priority than old template body and target-region HTML. - Tell the AI not to carry over default gallbladder or liver descriptions when the doctor asks for another procedure or body part. - Limit old full-report context length to reduce anchoring on default template content. - Update README, AGENTS, report editor, design, progress, and testing docs for the AI prompt priority model. - Add unit tests covering AI prompt priority and old-content truncation. --- AGENTS.md | 1 + README.md | 1 + docs/design.md | 2 +- docs/modules/report-editor.md | 10 ++++++ docs/progress.md | 1 + docs/testing.md | 1 + src/pages/ReportEditor.tsx | 21 ++++++++---- src/utils/aiPrompt.test.ts | 37 ++++++++++++++++++++ src/utils/aiPrompt.ts | 63 +++++++++++++++++++++++++++++++++++ 9 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/utils/aiPrompt.test.ts create mode 100644 src/utils/aiPrompt.ts diff --git a/AGENTS.md b/AGENTS.md index c1c4dba..00e0710 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -351,6 +351,7 @@ PostgreSQL 数据模型。当前覆盖 `Tenant`、`Department`、`User`、`UserS - 后端模板 DTO 和权限资源映射 - 模板列表合并工具,防止新增模板被旧 `localStorage.templates` 覆盖 - 模板导入导出工具,覆盖 HTML 模板包生成、字段库元数据回导和旧 JSON 导入兼容 +- AI Prompt 生成工具,确保医生指令和结构化报告字段优先于默认模板旧内容 - 后端用户 DTO 和部门模板授权映射 - 后端系统设置 schema 校验 - 后端 AI 入参和讯飞语音代理帧处理 diff --git a/README.md b/README.md index d4e08f1..f9978ae 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,7 @@ cp .env.example .env.local - 本地存储封装和系统设置兼容。 - 默认报告模板结构和字段配置。 - 模板 HTML 导出包字段库元数据。 +- AI Prompt 优先级,避免默认模板旧术式覆盖医生新指令。 - 打印导出入口。 - 后端权限策略、AI 入参和语音代理帧处理。 diff --git a/docs/design.md b/docs/design.md index 0ec2374..d6c2cee 100644 --- a/docs/design.md +++ b/docs/design.md @@ -90,7 +90,7 @@ src/ - `data-mode="frame"` 或无 `manual` 标记的占位符可接收关键帧。 - `data-mode="manual"` 用于 Logo、签名等静态图片,不接收视频关键帧拖入。 -AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位。AI 返回 `updatedHtml` 后,系统先显示差异确认,再注入目标区域。 +AI 可编辑区域通过 `.ai-region[data-ai-id]` 和内部 `.ai-content` 定位。AI 返回 `updatedHtml` 后,系统先显示差异确认,再注入目标区域。AI prompt 会把医生指令和结构化报告字段放在最高优先级,旧目标区域 HTML 和整份报告正文只作为低优先级参考,避免默认模板中的旧术式描述持续影响新生成内容。 ## 打印导出设计 diff --git a/docs/modules/report-editor.md b/docs/modules/report-editor.md index a8e678f..d65b218 100644 --- a/docs/modules/report-editor.md +++ b/docs/modules/report-editor.md @@ -71,6 +71,16 @@ AI 面板支持两种模式: - 对话模式:根据当前报告内容和图片上下文回答问题。 - 修改模式:选中 `.ai-region` 后要求模型返回 JSON,其中包含 `reply` 和 `updatedHtml`。 +AI 提议版本由以下内容生成: + +- 医生在 AI 输入框中的指令。 +- 当前结构化报告字段,包括手术名称、术前诊断、术中/术后诊断、麻醉方式和报告备注。 +- 整份报告正文文本,作为低优先级上下文。 +- 当前选中的 AI 可编辑区域 HTML,作为低优先级格式和旧内容参考。 +- 用户选择的报告图片或上传给 AI 的图片。 + +Prompt 明确规定优先级为:医生指令 > 当前结构化报告字段 > 图片内容 > 旧目标区域/整份报告正文。默认模板中的胆囊、肝脏等旧术式描述不得覆盖医生新指令;如果医生要求生成新的术式或不同部位,AI 应围绕新术式重写目标区域。 + 编辑器会监听正文中的 `.ai-region` 变化;用户插入新的 AI 可编辑区域后,AI 撰写面板的目标下拉栏会立即更新,不需要离开页面或刷新。 模型返回 HTML 后,系统会清理换行和 `
`,生成差异预览。用户确认后才写入目标 `.ai-content`。 diff --git a/docs/progress.md b/docs/progress.md index 8a984ce..b4d0638 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -89,3 +89,4 @@ | 2026-05-02 | 修复报告编辑器新增 AI 可编辑区域后 AI 撰写下拉栏不立即更新的问题,并补充 E2E。 | | 2026-05-02 | 移除前端用户可见 JSON 导出入口,保留模板历史 JSON 导入兼容。 | | 2026-05-02 | 模板 HTML 导出包补充模板字段和字段管理设置,导入时恢复字段库元数据。 | +| 2026-05-02 | 调整报告编辑器 AI Prompt 优先级,医生指令和结构化报告字段优先于默认模板旧术式内容。 | diff --git a/docs/testing.md b/docs/testing.md index d12fe4a..5f8df82 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -82,6 +82,7 @@ AI 第三方接口、讯飞语音上游 WebSocket、麦克风权限和真实视 | 系统设置混淆兼容 | 已覆盖 | `storage.test.ts` | | 默认报告模板结构 | 已覆盖 | `defaultContent.test.ts` | | 默认字段和 AI Provider | 已覆盖 | `defaultContent.test.ts` | +| AI Prompt 优先级 | 已覆盖 | `aiPrompt.test.ts`,覆盖医生指令和结构化字段优先于旧模板内容。 | | 打印导出入口 | 已覆盖 | `print.test.ts` | | 模板列表合并工具 | 已覆盖 | `templateList.test.ts`,防止新增模板被旧本地缓存覆盖。 | | 模板导入导出工具 | 已覆盖 | `templateExport.test.ts`,覆盖 HTML 模板包、字段库元数据回导、旧 JSON 导入兼容和文件名清理。 | diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index 524f70d..2111a9b 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -22,6 +22,7 @@ import { getSpeechIatWebSocketUrl } from '../api/speech'; import { getFieldLibrary, updateFieldLibrary } from '../api/library'; import { listFiles, uploadFileResource } from '../api/files'; import { isLocalFallbackEnabled } from '../config/runtime'; +import { buildSurgicalAiPrompt } from '../utils/aiPrompt'; import { diffChars } from 'diff'; type AudioWindow = Window & typeof globalThis & { @@ -1284,14 +1285,20 @@ export default function ReportEditor() { } const currentHtml = targetRegionEl ? targetRegionEl.innerHTML.replace(/​/g, '').replace(/>(\s+)<').trim() : ''; const globalContextText = editorRef.current?.innerText || ''; + const targetTitle = aiRegions.find(region => region.id === actualTargetId)?.title + || (aiRegion as HTMLElement | null)?.dataset.aiTitle + || actualTargetId; let messageContent: any; const selectedEditorImageUrls = editorImages.filter(img => aiSelectedEditorImages.includes(img.id)).map(img => img.src); const allImages = [...selectedEditorImageUrls, ...aiUploadedImages.map(i => i.dataUrl)]; - let promptText = `【全局手术报告参考内容】:\n${globalContextText}\n\n`; - if (aiModifyEnabled && targetRegionEl) { - promptText += `【你需要进行修改的目标区域 HTML 源码】:\n${currentHtml || '(当前区域为空)'}\n\n`; - } - promptText += `【医生指令】: ${text}\n\n【格式要求】:\n1. 仅针对【目标区域】的主题生成对应的多段落 HTML 内容\n2. ⚠️ 绝对禁止将【全局参考内容】中的其他模块(如:基本信息、术后情况、标本描述、病理结果、医生签名、日期等)生成并混入你的输出中!你只能输出该区域应有的内容\n3. 段落使用

标签,段落之间不要使用
标签或换行符\n4. 输出紧凑 HTML,标签间不要有空格或换行`; + const promptText = buildSurgicalAiPrompt({ + reportData, + globalContextText, + currentHtml, + doctorInstruction: text, + modifyEnabled: aiModifyEnabled && Boolean(targetRegionEl), + targetTitle, + }); if (allImages.length > 0) { messageContent = []; allImages.forEach(url => { @@ -1302,8 +1309,8 @@ export default function ReportEditor() { messageContent = promptText; } const systemPrompt = aiModifyEnabled - ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据用户的【医生指令】,直接重写并输出目标区域的 HTML。\n\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 【内容边界】:全局参考内容仅供你理解上下文。你的 updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n4. 段落必须使用

标签包裹,段落间绝对不要使用
标签,也不要使用换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json)' - : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释'; + ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n请根据用户的【医生指令】直接重写目标区域 HTML。优先级必须是:医生指令 > 当前结构化报告字段 > 图片内容 > 旧目标区域/整份报告参考文本。\n\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 如果医生要求生成新的术式、其他手术或不同部位,必须抛开旧目标区域中的默认模板内容,不要继续写胆囊、肝脏等旧术式描述\n4. 【内容边界】:全局参考内容仅供理解上下文。updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n5. 段落必须使用

标签包裹,段落间绝对不要使用
标签,也不要使用换行符 (\\n)\n6. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n7. 绝对不要包含任何 Markdown 标记(如 ```json)' + : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请根据【医生指令】进行专业解答。优先级必须是:医生指令 > 当前结构化报告字段 > 图片内容 > 旧正文参考文本;如果医生要求新的术式或不同部位,不要被旧正文中的胆囊、肝脏等默认模板内容带偏。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记\n5. 无论图片内容是什么,请严格以纯 JSON 格式输出,禁止任何多余的开头解释'; const payload: any = { model: modelName, messages: [ diff --git a/src/utils/aiPrompt.test.ts b/src/utils/aiPrompt.test.ts new file mode 100644 index 0000000..950f90e --- /dev/null +++ b/src/utils/aiPrompt.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { buildSurgicalAiPrompt } from './aiPrompt'; + +describe('buildSurgicalAiPrompt', () => { + it('makes doctor instructions and structured fields higher priority than old template content', () => { + const prompt = buildSurgicalAiPrompt({ + reportData: { + title: '腹腔镜阑尾切除术报告', + preoperativeDiagnosis: '急性阑尾炎', + postoperativeDiagnosis: '急性化脓性阑尾炎', + }, + globalContextText: '默认模板旧内容:肝脏色红质软,遂行腹腔镜胆囊切除术。', + currentHtml: '

腹腔镜探查肝脏,切除胆囊。

', + doctorInstruction: '生成阑尾切除术的手术步骤', + modifyEnabled: true, + targetTitle: '手术步骤', + }); + + expect(prompt.indexOf('医生指令(最高优先级)')).toBeLessThan(prompt.indexOf('旧目标区域 HTML 源码')); + expect(prompt).toContain('手术名称: 腹腔镜阑尾切除术报告'); + expect(prompt).toContain('如果医生指令要求生成新的术式'); + expect(prompt).toContain('不要沿用旧目标区域中的胆囊、肝脏或其他默认模板内容'); + }); + + it('truncates long global context to reduce old-content anchoring', () => { + const prompt = buildSurgicalAiPrompt({ + reportData: {}, + globalContextText: '旧'.repeat(5000), + currentHtml: '', + doctorInstruction: '生成新的手术步骤', + modifyEnabled: false, + }); + + expect(prompt.length).toBeLessThan(4700); + expect(prompt).toContain('...'); + }); +}); diff --git a/src/utils/aiPrompt.ts b/src/utils/aiPrompt.ts new file mode 100644 index 0000000..bab7ef0 --- /dev/null +++ b/src/utils/aiPrompt.ts @@ -0,0 +1,63 @@ +import { Report } from '../types'; + +const cleanText = (value: unknown) => { + if (Array.isArray(value)) return value.filter(Boolean).join('、'); + return String(value ?? '').replace(/\s+/g, ' ').trim(); +}; + +const truncateText = (value: string, maxLength = 4000) => + value.length > maxLength ? `${value.slice(0, maxLength)}...` : value; + +export interface SurgicalAiPromptInput { + reportData: Partial & { + preoperativeDiagnosis?: unknown; + postoperativeDiagnosis?: unknown; + }; + globalContextText: string; + currentHtml: string; + doctorInstruction: string; + modifyEnabled: boolean; + targetTitle?: string; +} + +export const buildSurgicalAiPrompt = ({ + reportData, + globalContextText, + currentHtml, + doctorInstruction, + modifyEnabled, + targetTitle, +}: SurgicalAiPromptInput) => { + const structuredFields = [ + ['手术名称', reportData.title], + ['术前诊断', reportData.preoperativeDiagnosis], + ['术中/术后诊断', reportData.postoperativeDiagnosis], + ['麻醉方式', reportData.anesthesiaType], + ['报告备注', reportData.reportNote], + ] + .map(([label, value]) => { + const cleaned = cleanText(value); + return cleaned ? `${label}: ${cleaned}` : ''; + }) + .filter(Boolean) + .join('\n') || '暂无结构化字段'; + + let promptText = `【医生指令(最高优先级)】:\n${doctorInstruction}\n\n`; + promptText += `【当前结构化报告字段(高优先级,若与旧正文冲突,以这里和医生指令为准)】:\n${structuredFields}\n\n`; + promptText += `【整份报告参考文本(低优先级,仅用于患者信息、术式背景和上下文;若与医生指令或结构化字段冲突,必须忽略这里的旧术式描述)】:\n${truncateText(globalContextText)}\n\n`; + + if (modifyEnabled) { + promptText += `【目标区域】:\n${targetTitle || '未命名区域'}\n\n`; + promptText += `【旧目标区域 HTML 源码(只作为格式、段落风格和可复用事实参考;不得因为旧内容存在胆囊、肝脏等描述就继续沿用,除非医生指令或结构化字段明确要求)】:\n${currentHtml || '(当前区域为空)'}\n\n`; + } + + promptText += `【格式要求】: +1. 优先执行【医生指令】,其次参考【当前结构化报告字段】,旧目标区域和整份报告文本优先级最低 +2. 如果医生指令要求生成新的术式、其他手术或不同部位,必须围绕新术式重写,不要沿用旧目标区域中的胆囊、肝脏或其他默认模板内容 +3. 仅针对【目标区域】的主题生成对应的多段落 HTML 内容 +4. 绝对禁止将【整份报告参考文本】中的其他模块(如:基本信息、术后情况、标本描述、病理结果、医生签名、日期等)生成并混入输出 +5. 段落使用

标签,段落之间不要使用
标签或换行符 +6. 输出紧凑 HTML,标签间不要有空格或换行`; + + return promptText; +};