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 标签,段落之间不要使用
标签或换行符
+6. 输出紧凑 HTML,标签间不要有空格或换行`;
+
+ return promptText;
+};