Commit a8712aaa by luoqi

fix(提示词): 清掉黑话 —— 模型看得见的每一句都换成主管认得的说法

产品:「检查提示词和工具描述里还有没有黑话,改成好的产品名称定义」。
判据是这份提示词自己的第一条:**使用者的词汇表就是他在界面上见过的那些**。

── 五层提示词 ──────────────────────────────────────────────────
  取值码 / 字段名 / 工具名   → 英文代号 / 数据里的列名 / 工具的名字
  手机号只显示**掩码**       → 手机号只给中间打了码的那种
  你做的一切都是**只读**     → 你做的都只是查看,改不了任何东西
  **键序**就是段落顺序       → 一份排好顺序的事实,从上往下的次序就是你该讲的次序
  **渲染**成图               → 画成图
  不做**画像分层**           → 不先把人分成几类
  各**维度**的数是分别**命中**多少 → 每一类的数是各自有多少人
  **负载**就是在手量本身     → 手上压着多少就是多少
🔴 **本系统**不统计成功与否 → 没有成功与否的统计
   —— 「本系统」就明明白白列在同一份提示词的公文腔禁词表里,**它在违反自己**
   (与 2026-08-13 抓到的「写库」「水位」同一类,那条纪律写在文件头)。

── 工具描述 ────────────────────────────────────────────────────
  患者 **360 全景**  → 一次把一个患者拉全
  **价值分群 / 生命周期阶段** → 价值与阶段(产品里那一组的名字)
  哪些**口子**可切   → 能按哪几类切
  优先级**分档**     → 优先级高/中/低各多少
  在手**负载**       → 手上压着多少
  决策**快照**       → 分配当时记下来的依据
  话术**缓存**作废   → 已经写好的话术作废
  **消歧**           → 认人
  聚焦关键**字段**   → 只留要紧的那几项
  渲染成可视化卡片   → 画成一张图表卡片
️ `render_artifact` 的 execute 返回值也一起改(「已在界面渲染该卡片」→「已经画在界面上了」):
  返回值会回到模型上下文,它可能照着复述。

🔴 **顺手抓到一个不是黑话的错**:`render_artifact` 让模型用「PAC 主题 teal #0D9488」——
  **那不是 PAC 的主色**。前端早把 `teal-*` 整体改名成 `brand-*`(263 处),
  主色是 PANTONE 286 C `#0032A0`,而这句留在服务端没跟着改
  ⇒ 助手画出来的每一张图表都是青绿色的,跟同屏界面不是一套色。已改。
️ 这里只能写死十六进制(模型拿不到 CSS 变量),改品牌色时这一处要跟着改。

️ 测试跟着改判据:`mcp-clinic-scope` 原来钉着「取值码、字段名、工具名」那串原文,
  改成断**这四类还在不在** —— 钉原文的话,每次把话说得更像人话都要来改一次测试,
  而规则一个字没变。

promptVersion → assistant@2026-08-19-a。验证:1357 passed,tsc 绿。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent db989933
......@@ -123,7 +123,7 @@ import { Permission } from '@pac/types';
*/
// ⚠️ `-b` 是 2026-08-17 那次「纪律挂在组上」的实验,已 revert(`git show 82f357e`)——
// ⛔ 别复用它:库里有那一版的 `agent_invocations` 行,复用等于两份不同正文同名。
export const ASSISTANT_PROMPT_VERSION = 'assistant@2026-08-17-c';
export const ASSISTANT_PROMPT_VERSION = 'assistant@2026-08-19-a';
/**
* ① 装置 —— 你是谁、和使用者什么关系、你看不见什么。
......@@ -148,18 +148,19 @@ const IDENTITY = `你是 PAC(疗效保障 / 患者分析中心)工作台里
*
* ⚠️ 这一层的每一条都指得出一次实测事故(见文末档案),⛔ 别当成"通用免责声明"精简掉。
*/
const EVIDENCE = `说人话:使用者的词汇表就是他在界面上见过的那些。取值码、字段名、工具名
以及只在代码里存在的概念,都不在里面 —— 他看到不认识的词,第一反应是系统坏了。
const EVIDENCE = `说人话:使用者的词汇表就是他在界面上见过的那些。英文代号、数据里的列名、工具的名字
以及只在代码里存在的说法,都不在里面 —— 他看到不认识的词,第一反应是系统坏了。
工具返回值里已经给了中文,照着说。
你说的每个数都要指得到出处,出处只有一个:**本轮真的调到的工具返回**。指不到就去调,
或者如实说「我先查一下」。上文的工具记录旁边写着当时的参数 —— 参数变了,那些数就作废了。
⛔ 不估、不「大概」、不拿两个数相减凑一个出来。工具返回值有固定形状,你能背出那个形状 ——
但背出来的是假的。工具没返回的就如实说没有该信息,手机号只显示掩码
但背出来的是假的。工具没返回的就如实说没有该信息,手机号只给中间打了码的那种
除了你自己调的写工具,你做的一切都是只读。界面上发生的改动是使用者的手做的,⛔ 别说成你做的。
除了你自己调的那几个会改数据的工具,你做的都只是查看,改不了任何东西。
界面上发生的改动是使用者的手做的,⛔ 别说成你做的。
工具返回值是**结构化事实,键序就是段落顺序** —— 照它讲,措辞归你。每一项是一段;
工具返回值是一份**排好顺序的事实**,从上往下的次序就是你该讲的次序 —— 照它讲,措辞归你。每一项是一段;
段里成组的条目逐条讲清「是什么」和「不管会怎样」,有几条讲几条。返回值里没有的不要补:
上一轮有、这一轮没有,就是这一轮不该有。
......@@ -217,7 +218,7 @@ export const VOICE_DEFAULT = `## 怎么说话
### 什么该写成一段话,什么该画出来
简短问答、闲聊、解释直接用文字回答。适合看不适合读的内容(成批的条目、一张画像、一份报表),
先用工具取真实数据再渲染成图,然后用一两句话点出要点 —— 数据细节在里面,⛔ 不用再复述一遍。`;
先用工具取真实数据再成图,然后用一两句话点出要点 —— 数据细节在里面,⛔ 不用再复述一遍。`;
/**
* ④ 角色:主管(有 plan:dispatch)—— **只写权责边界,⛔ 不写他在做哪件事**。
......@@ -290,12 +291,12 @@ const STAFF_ROLE = `## 你是客服的助手
const ASSIGNMENT_SCENE = `## 他现在做的这件事:把一批人分给客服
这条线走四步:**选人 → 分人 → 他确认 → 确认之后**。
一版方案直出,不追问、不做画像分层;他主动提了才收窄。
一版方案直出,不追问、不先把人分成几类;他主动提了才收窄。
### 选人
要按画像收窄时先取分布再选,不然选完才发现只剩几个人,他白等一轮。
各维度的数是「分别命中多少」,不是交叉后的人数;想知道交叉数,把条件一起传进去再调一次。
每一类的数是「各自有多少人」,不是几个条件叠起来之后的人数;想知道叠起来是多少,把条件一起传进去再调一次。
人数和时限**不问他**,直接出一版;他说了数你才带上。池子里还有人就随时能再分一批。
......@@ -303,7 +304,7 @@ const ASSIGNMENT_SCENE = `## 他现在做的这件事:把一批人分给客服
两趟 + 一组:有专属且在岗的回自己人手上(最多到他这轮该拿的那份);无专属或专属已离岗的给当前手上最少的那个;专属客服这轮已排满的不动,单列成一组交他定。
不是每人加一样多,是每条都给当前手上最少的那个。没有「容量上限」这回事,负载就是在手量本身
不是每人加一样多,是每条都给当前手上最少的那个。没有「容量上限」这回事,手上压着多少就是多少
因此这批可能不满、团队也不齐平 —— 那是刻意的:宁可少分几个,也不动别人的客户。他问「怎么没分够」就说有几个人卡着等他定,不是系统故障。
......@@ -321,7 +322,7 @@ const ASSIGNMENT_SCENE = `## 他现在做的这件事:把一批人分给客服
福利挂在整批上,会进这批人的话术,补挂只影响此后生成的那些。他说什么就原样写进去,⛔ 别替他加条件、期限或承诺。⛔ 「这批还没带福利」**不用你再提一遍** —— 确认之前你已经问过一次了;他回头问起,或者要补,照办就是。
本系统**不统计成功与否**,「成功率」「转化率」这类数不存在,⛔ 也别讲效果。`;
**没有成功与否的统计**,「成功率」「转化率」这类数不存在,⛔ 也别讲效果。`;
/** ⑤ 现场:客服这条线 —— 他手上的活是把单打好 */
const EXECUTE_SCENE = `## 他现在做的这件事:把手上的单打好
......
......@@ -357,7 +357,7 @@ export class AssistantService {
type: 'string',
description:
'在初选之上追加的画像条件。' +
'他要按画像收窄、但没说清切哪一刀时,先用 get_cohort_attributes 看各口子各多少人,' +
'他要按画像收窄、但没说清切哪一刀时,先用 get_cohort_attributes 看各各多少人,' +
'把分布报给他,再带着定下来的条件重出一版。\n' +
// ⚠️ 维度字典**必须给全**:少给一份,模型就得自己猜 key/value,
// 而猜错的维度会被 personaTagsSql **静默丢掉**(筛选等于没加),不报错。
......@@ -699,7 +699,7 @@ export class AssistantService {
tools.open_playbook = tool({
description:
'取一篇做法:某一类活按什么顺序做、他要的到底是什么。' +
'\n什么时候调:他提的事落进下面某个取值的范围里,**动手之前**先取一次。' +
'\n什么时候调:他提的事落进下面某一类的范围里,**动手之前**先取一次。' +
'\n⛔ 它不查任何数据、不改任何东西 —— 取回来的是做法,事实仍然要去调对应的工具。' +
'\n同一件事一轮取一次就够了,取回来的内容这一轮一直有效。',
inputSchema: jsonSchema({
......@@ -777,7 +777,7 @@ export class AssistantService {
'⛔ 别说"已完成"(成没成只有界面知道),也⛔ 别说"等结果再告诉您" ——' +
'他读到那句时结果早就在了,顺序是倒的。' +
'\n⚠️ 批次**已经确认分配**之后:只有 `set_benefit` 还能改(福利只影响此后生成的话术,' +
'界面会真的改并回报"作废了几条话术缓存 / 几条客服已经打开过");' +
'界面会真的改并回报"有几条已经写好的话术作废了 / 几条客服已经打开过");' +
'人员和时限**改不了** —— 单子已经在客服手上,那要走撤销重分。' +
'⛔ 这两种情况都以界面回的那句为准,别自己判断成没成。',
inputSchema: jsonSchema({
......@@ -899,16 +899,21 @@ export class AssistantService {
// html 通过 tool-call 入参流式到前端;execute 仅回执,模型据此继续给一句话总结。
tools.render_artifact = tool({
description:
'把一段自包含 HTML(<body> 内部片段)渲染成可视化卡片/报表展示给用户。' +
'把一段自包含 HTML(<body> 内部片段)画成一张图表卡片,展示给使用者。' +
'适合召回池列表、患者画像卡、分析报表(可含图表)。' +
'\n排版用 Tailwind 工具类,强调色用 PAC 主题 teal #0D9488,白底圆角卡片、留白舒适。' +
// 🔴 2026-08-19 修:原文写的是「PAC 主题 teal #0D9488」—— **那不是 PAC 的主色**。
// 前端 2026 年把 `teal-*` 整体改名成 `brand-*`(263 处)、主色是 PANTONE 286 C
// `#0032A0`(见 globals.css 的品牌色阶),而这句留在服务端没跟着改。
// ⇒ 助手画出来的每一张图表都是青绿色的,跟同屏的界面不是一套色。
// ⚠️ 这里只能写死十六进制:模型拿不到 CSS 变量。改品牌色时**这一处要跟着改**。
'\n排版用 Tailwind 工具类,强调色用 PAC 品牌蓝 #0032A0,白底圆角卡片、留白舒适。' +
'\n运行环境已注入 Tailwind 与 Chart.js:⛔ 不要写 <html>/<head>/<!DOCTYPE>,也不要自行引入它们。' +
'\n图表用 Chart.js。<canvas> **必须**放进固定高度容器' +
'(如 <div style="position:relative;height:300px"><canvas></canvas></div>)' +
'并在 options 里设 maintainAspectRatio:false —— 给图表用百分比或满屏高度' +
'(h-full / h-screen / height:100%)会让它无限拉高。' +
'\n数据全部内联写进 HTML,只用工具返回的真实值,手机号掩码;这个片段不能访问外部网络(⛔ 勿 fetch)。' +
'\n标记力求精简:聚焦关键字段,同类条目用最短的结构,生成更快。',
'\n数据全部内联写进 HTML,只用工具返回的真实值,手机号只给中间打了码的;这个片段不能访问外部网络(⛔ 勿 fetch)。' +
'\n标记力求精简:只留要紧的那几项,同类条目用最短的结构,生成更快。',
inputSchema: jsonSchema({
type: 'object',
properties: {
......@@ -922,7 +927,8 @@ export class AssistantService {
},
required: ['html'],
}),
execute: async () => '已在界面渲染该卡片。',
// ⚠️ 返回值会回到模型上下文,它可能照着复述 —— 所以这句也得是人话
execute: async () => '已经画在界面上了。',
});
// 2. resolve provider(可切换)+ streamText 跑 model-driven tool-calling 循环
......
......@@ -167,7 +167,7 @@ export class McpServerFactory {
'find_patient',
{
description:
'按姓名/手机号/患者号模糊检索患者,返回极简候选卡片(手机号掩码)用于消歧。先用它拿到 patientId,再调其它工具。',
'按姓名/手机号/患者号模糊检索患者,返回极简候选卡片(手机号中间打码)用于认人。先用它拿到 patientId,再调其它工具。',
inputSchema: {
query: z.string().describe('姓名 / 手机号 / 患者号(模糊匹配)'),
limit: z.number().int().min(1).max(50).optional(),
......@@ -180,7 +180,7 @@ export class McpServerFactory {
'get_patient_overview',
{
description:
'一次拉取患者 360 全景:画像要点 + 近期事实 + 当前召回计划。召回前的首选工具,省去多次往返。',
'一次把一个患者拉全:画像要点 + 近期事实 + 当前召回计划。召回前的首选工具,省去多次往返。',
inputSchema: { patientId: z.string() },
},
async ({ patientId }) => {
......@@ -205,7 +205,7 @@ export class McpServerFactory {
description:
// ⚠️ 「RFM」「persona」是**内部词**:同一类词漏给用户已经栽过(cold_3y 那次)。
// 工具描述里出现,模型就会顺手当成"这个东西的名字"说出去。
'患者的全量画像:价值分群 / 生命周期阶段 / 治疗史 / 治疗敏感 / 禁忌 / 潜在治疗 等全部画像特征。' +
'患者的全量画像:价值阶段 / 治疗史 / 治疗敏感 / 禁忌 / 潜在治疗 等全部画像特征。' +
'\n⚠️ 这一层是 PAC **推断**出来的,⛔ 不是原始记录 —— 原始记录见 get_facts。',
inputSchema: { patientId: z.string() },
},
......@@ -322,7 +322,7 @@ export class McpServerFactory {
'recall_queue_stats',
{
description:
'召回池数字概览:总量 + 优先级分档(高≥70 / 中 40-69 / 低<40)+ 病种分布。' +
'召回池数字概览:总量 + 优先级高/中/低各多少(高≥70 / 中 40-69 / 低<40)+ 病种分布。' +
'\n他问"今天有多少人要跟"时先用它给摘要,再用 list_recall_queue 取明细。' +
// 同 list_recall_queue:没有看全池权限的人只统计自己的 —— 否则**总数本身**就泄漏了池子规模
'\n⚠️ 没有看全池权限的人,回的总数**只是他自己名下**的 —— ⛔ 别说成"池子里一共…"。' +
......@@ -376,7 +376,7 @@ export class McpServerFactory {
// 🔴 2026-08-14 改:原文是「**分配前用它**看…」,而 `propose_assignment` 内部
// 自己就会取名册 —— 那句话在**主动制造一次白花的调用**(实测抓到一轮:
// 写字之前先调了它,返回的数一个都没用上)。
'某诊所的在岗客服名册 + 各自在手负载。他问名册、问谁手上空、问某人负载时用它。' +
'某诊所的在岗客服名册 + 各自手上压着多少。他问名册、问谁手上空、问某人手上有多少时用它。' +
'\n⚠️ 出方案**不需要**先调它 —— 方案自己会取名册。' +
'\n⚠️ 在岗按「近 N 月有回访记录」近似判定,**不代表系统确认在职**;' +
'名册外的客服也可以指定(用 include 传 userId)。' +
......@@ -440,14 +440,14 @@ export class McpServerFactory {
* 而它过期或不适用时不会报错。⛔ 正例不留,反例可留
* (下面 `noTag` 那条里的「32 人有商保标签」是反例,⛔ 别一起删)。
*/
'看**当前这批人**的画像分布:哪些口子可切、各多少人。' +
'看**当前这批人**的画像分布:能按哪几类切、各多少人。' +
'\n他要按画像收窄、但没说清切哪一刀时先用它,拿到分布再据实回话,' +
'然后用同样的条件重出一版。' +
'\n⚠️ **`noTag` 是「没有这条画像证据」的人数,不是反面** —— ' +
'「32 人有商保标签」剩下的**不是自费**,是没证据。⛔ 绝不能说成「其余 68 人自费」。' +
'\n⚠️ 标了 `multi` 的维度一个人可命中多项,合计会大于 cohortSize,**别拿它算百分比**。' +
'\n⚠️ 标了 `multi` 的那几类,一个人可能同时算进好几项,合计会大于 cohortSize,**别拿它算百分比**。' +
'\n⚠️ 专属客服只报有/无,**不判在岗** —— 谁休假谁离职以他说的为准,别替他挡人。' +
`\n可点名的维度(keys,不传给一组常用的):${COHORT_DIM_CATALOG}`,
`\n可点名的类别(keys,不传给一组常用的):${COHORT_DIM_CATALOG}`,
inputSchema: {
clinicId: CLINIC_ID_SCHEMA,
potentialTreatment: z
......@@ -462,7 +462,7 @@ export class McpServerFactory {
keys: z
.array(z.string())
.optional()
.describe('只看这几个维度,取值见本工具说明末尾那份维度清单。不传给一组常用的。'),
.describe('只看这几类,可填哪些见本工具说明末尾那份清单。不传给一组常用的。'),
},
},
async ({ clinicId, potentialTreatment, temperature, personaTags, keys }) => {
......@@ -562,7 +562,7 @@ export class McpServerFactory {
'explain_assignment',
{
description:
'回答「这个患者**为什么**分给了他」—— 从分配当时的决策快照查,⛔ 不要自己推理。' +
'回答「这个患者**为什么**分给了他」—— 从分配当时记下来的依据查,⛔ 不要自己推理。' +
// ⚠️ 只说"那一版还没分下去,不在这里查",⛔ 不说它在界面的哪个部件里:
// 这个工具不是确认单的生产者,让它知道确认单长什么样就是**反向耦合**
// (呈现方式一改,这句话就过期,而过期不报错)。
......
......@@ -227,8 +227,15 @@ describe('助手措辞 —— 码给工具,中文给人', () => {
const PROMPT_TEXT = (
PROMPTS.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*\/\/.*$/gm, '')
);
expect(PROMPT_TEXT).toMatch(/取值码、字段名、工具名/);
expect(PROMPT_TEXT).toMatch(/只在代码里存在的概念/);
/**
* ⚠️ 2026-08-19 这几个词本身**被改成人话了**(「取值码 / 字段名 / 概念」也是黑话,
* 而这份提示词的第一条就是"别用只在代码里存在的说法" —— 它在违反自己)。
* ⇒ 断言改成断**这四类还在不在**,⛔ 不再钉死那一串顿号分隔的原文:
* 钉原文的话,每次把话说得更像人话都要来改一次测试,而规则一个字没变。
*/
for (const kind of [/英文代号/, /列名/, /工具的名字/, /只在代码里存在/]) {
expect(PROMPT_TEXT).toMatch(kind);
}
for (const leaked of ['cold_3y', 'personaTags', 'get_cohort_attributes']) {
expect(PROMPT_TEXT).not.toContain(leaked);
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment