Commit 875a406c by luoqi

feat(引导): 每条引导上带一份「一类一调」的调用纪律

产品 2026-08-17 把这句话手工粘到 `标识` 的值里试了一轮,**模型的排布确实变了**
⇒ 收进代码,独立成 `开放方式` 字段,紧跟 `标识`。

️ 挂在**每一条**上, 不是每一组上:纪律管的是「这一条讲完、紧接着调这一条」,
  作用域就是一条 —— 挂在组上时一组里的两条仍会被攒着一起调(上一版实验就是这么做的,
  已 revert:`git show 82f357e0`)。
️ 位置紧跟标识:产品实测有效的那一版就是粘在标识后面的,
   别挪到末尾"省一点注意力" —— 那等于换了个没测过的位置。

🔴 这是事实层「 一个字的指令都不许有」的**唯一一处破例**,所以破得有边界:
  · 措辞与 `show_guidance` 描述同源(都用「开放」、都说「一类」)—— 别两处各发明一套
  ·  不写工具名:代码标识符进事实层早晚被念给主管听(沿革见 GUIDANCE_KIND)
  · 禁词字面照旧全域禁,那句纪律自己也得守(单独一条测试盯着)
  · promptVersion → -c(-b 是被 revert 的那次实验,库里有它的行, 不复用)

⇒ 怎么验它还在起作用:`agent_invocations.output.steps`(stepShapes)——
  几条引导落在各自独立、且 textLen>0 的步里 = 守住了;挤在同一步 = 没守住。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent df9bc7fc
......@@ -113,13 +113,17 @@ import { Permission } from '@pac/types';
* · ①〜⑤ 任意一层的正文
* · **工具描述**(它占模型上下文的大头 —— 实测每轮约 11K token,比五层还多)
* · **给模型的固定标识**(如 `GUIDANCE_KIND` 的中文名,它会出现在模型嘴里)
* · **事实层里的固定文本**(如 `assignment-facts.ts` 的 `GUIDANCE_HOWTO`)——
* 它每轮一模一样、且模型看得见,与工具描述同性质。
* ⚠️ 原来这条只写了"五层",于是改工具描述不 bump、版本号照旧 ——
* 回放时"当时那份提示词长什么样"就对不上了,而且不会有任何报错。
* ⚠️ 系统提示词**不逐行落库**:主管那份 6.9 KB 每次一模一样,逐行存等于把同一份东西
* 抄一万遍。审计靠「这一行的 promptVersion + 代码里那个版本的正文」两边对 ——
* ⇒ 版本不 bump,这条链就断了,而断了不会有任何报错。
*/
export const ASSISTANT_PROMPT_VERSION = 'assistant@2026-08-17-a';
// ⚠️ `-b` 是 2026-08-17 那次「纪律挂在组上」的实验,已 revert(`git show 82f357e`)——
// ⛔ 别复用它:库里有那一版的 `agent_invocations` 行,复用等于两份不同正文同名。
export const ASSISTANT_PROMPT_VERSION = 'assistant@2026-08-17-c';
/**
* ① 装置 —— 你是谁、和使用者什么关系、你看不见什么。
......
......@@ -29,10 +29,44 @@ import { GUIDANCE_KIND } from './assignment-signals';
* ⛔ **这里一个字的指令都不许有**(「请照抄」「不要说成…」之类)。
* 2026-08-08 栽过:`get_assignment_detail` 的 note 字段混着给模型的指令,
* 模型照抄就把内部指令原样贴进了主管的对话框。⇒ 事实归事实,规范归提示词。
* ⚠️ **例外一条、且只有一条**:每条引导上的 `开放方式`(见 `GUIDANCE_HOWTO`)。
* 2026-08-17 产品实测定的,破例的理由和它的边界都写在那个常量上。
* ⛔ 别照着它再开第二处 —— 一个"有过先例"的例外会变成没有边界。
* ⛔ 不含任何 planId / 患者姓名 —— 那些走侧信道给界面,模型手里不该有。
* ⚠️ 键名用中文:模型照着键名说就是对的,省掉一轮"把 inHandPending 翻译成人话"。
*/
/**
* 每条引导上都带一份的**调用纪律** —— `show_guidance` 要一类一调、紧挨着调。
*
* ═══ 为什么明知是指令还是要放在这一层 ═══════════════════════════════
* 同一条纪律 `show_guidance` 的**工具描述里已经有一份**,但那份离「这一版到底有哪几类」
* 隔着整个上下文。2026-08-17 产品把这句话手工粘到 `标识` 的值里试了一轮,
* **模型的排布确实变了** —— ⇒ 这不是"多说一遍更保险",是实测拿到的差别。
*
* ⚠️ 挂在**每一条**上,⛔ 不是每一组上:纪律管的是"这一条讲完、紧接着调这一条",
* 它的作用域就是一条。挂在组上时模型仍然可能把一组里的两条攒在一起。
* ⚠️ 位置紧跟 `标识`:产品实测有效的那一版就是粘在标识后面的,⛔ 别挪到末尾去
* "省一点注意力" —— 那等于换了个没测过的位置。
*
* ═══ 边界(破例只破这一处)═══════════════════════════════════════
* ⚠️ 措辞与 `show_guidance` 的描述**同源**(都用「开放」这个动词、都说「一类」)——
* ⛔ 别在这里另发明一套说法:两处打架时模型只会各取一半。
* ⛔ 不写工具名 `show_guidance`:这一层是给模型的**中文事实**,代码标识符进来
* 早晚会被念给主管听(沿革见 `assignment-signals.ts` 的 `GUIDANCE_KIND`)。
* 「开放」就是工具描述里的原词,不会有歧义。
* ⛔ 禁词表照旧管着这句话(「照抄」「原样」「绝不」「必须说」这些字面都不许出现)——
* 例外放行的是"一句指令",⛔ 不是把禁词表撕了。有测试盯着。
* ⚠️ 改这句话要 bump `ASSISTANT_PROMPT_VERSION`:它每轮一模一样、且模型看得见,
* 与工具描述同性质。
*
* ⇒ **怎么验它还在起作用**:`agent_invocations.output.steps`(`stepShapes`)——
* 几条引导落在各自独立、且 textLen>0 的步里 = 守住了;挤在同一步 = 没守住。
*/
const GUIDANCE_HOWTO =
'一类一调:讲完其中一类,紧接着开放这一类;再讲下一类,再开放一类。' +
'不许连着开放两类 —— 中间要隔着你讲下一类的话。';
/** 给模型的事实投影。⚠️ 纯函数,输入一份提案、输出一个可序列化对象。 */
export function modelFacts(p: AssignmentProposal): Record<string, unknown> {
/// ⚠️ **本批分到人的客服数**,⛔ 不是在岗名册人数 —— 名册走 `p.rosterCount`。
......@@ -49,12 +83,14 @@ export function modelFacts(p: AssignmentProposal): Record<string, unknown> {
* ⚠️ 标识放在**每条的第一位**:键序就是模型讲述的顺序,标识要先于内容出现。
* ⛔ 别把 `signal.key` 直接给模型(`pending` / `batch_size_basis` 是内部码)——
* 中文标识即使被念出口,读起来也是一句正常的话。
* ⚠️ `开放方式` 紧跟标识 —— 这一层唯一一处指令,理由和边界见 `GUIDANCE_HOWTO`。
*/
const pick = (stage: 'cohort' | 'size' | 'dispatch') =>
p.signals
.filter((s) => s.tier === 'action' && s.stage === stage)
.map((s) => ({
'标识': GUIDANCE_KIND[s.key] ?? s.key,
'开放方式': GUIDANCE_HOWTO,
'是什么': s.title,
'为什么': s.why,
'不处理会怎样': s.defaultLabel,
......
......@@ -493,7 +493,13 @@ describe('给模型的事实', () => {
expect(Object.keys(modelFacts(base()))).toEqual(['怎么选的', '怎么派的']);
});
test('🔴 ⛔ 里面一个字的指令都不许有', () => {
/**
* ⚠️ 2026-08-17 起这条边界有**一处具名例外**:每条引导上的 `开放方式`
* (`show_guidance` 的调用纪律,沿革与破例理由见 `assignment-facts.ts` 的 `GUIDANCE_HOWTO`)。
* ⇒ 例外放行的是"一句指令",⛔ 不是把禁词表撕了 —— 下面这几个字面**照旧全域禁**,
* 连那句纪律自己也得守(另有一条测试单独盯着它)。
*/
test('🔴 ⛔ 里面不许有指令性措辞(`开放方式` 那一句也不例外)', () => {
const json = JSON.stringify(modelFacts(base()));
for (const w of ['照抄', '原样', '不要说', '绝不', '⛔', '请把', '转述', '必须说']) {
expect(json).not.toContain(w);
......@@ -602,7 +608,8 @@ describe('给模型的事实', () => {
const f = modelFacts(p) as { 怎么派的: { 要你定的: Array<Record<string, unknown>> } };
expect(f.怎么派的.要你定的.length).toBeGreaterThan(0);
for (const x of f.怎么派的.要你定的) {
expect(Object.keys(x)).toEqual(['标识', '是什么', '为什么', '不处理会怎样']);
// ⚠️ `开放方式` 紧跟标识 —— 2026-08-17 加的调用纪律,沿革见 `GUIDANCE_HOWTO`
expect(Object.keys(x)).toEqual(['标识', '开放方式', '是什么', '为什么', '不处理会怎样']);
}
// ⛔ intent 技术细节不许进 —— 模型说出 `pending.to_owner` 只会吓到主管
expect(JSON.stringify(f.怎么派的.要你定的)).not.toMatch(/pending\.|expiry\.|cohort\./);
......@@ -692,6 +699,70 @@ describe('给模型的事实', () => {
for (const st of ['cohort', 'size', 'dispatch'] as const)
expect(titles(st).length).toBeGreaterThan(0);
});
/**
* 🔴 每条引导上的 `开放方式` —— 事实层**唯一一处指令**(2026-08-17 产品实测定的)。
* 沿革、破例理由、撤销条件都写在 `assignment-facts.ts` 的 `GUIDANCE_HOWTO` 上。
*
* ⚠️ 这几条锁的是**破例的边界**,⛔ 不是"这句话写得好不好":
* ① 挂在**每一条**上(纪律的作用域就是一条;挂在组上时一组里的两条仍会被攒着);
* ② 位置**紧跟标识**(产品实测有效的那一版就是粘在标识后面的,换位置 = 换了个没测过的东西);
* ③ 措辞与 `show_guidance` 描述同源,且⛔ 不含工具名;
* ④ 例外只放行"一句指令",禁词字面照旧不许出现。
*/
describe('每条引导上的「开放方式」', () => {
const items = () => {
const p = base();
p.pending = [{}] as AssignmentProposal['pending'];
p.signals = computeSignals(p, [
{
key: 'narrow',
severity: 3,
tier: 'action',
stage: 'cohort',
title: '这一格 1000 人,也可以只选其中一类',
why: '',
defaultLabel: '',
options: [],
},
]);
const f = modelFacts(p) as {
怎么选的: Record<string, unknown>;
怎么派的: Record<string, unknown>;
};
return [
...((f.怎么选的['这一格还能怎么选'] ?? []) as Array<Record<string, unknown>>),
...((f.怎么选的['本批人数还能怎么调'] ?? []) as Array<Record<string, unknown>>),
...((f.怎么派的['要你定的'] ?? []) as Array<Record<string, unknown>>),
];
};
test('🔴 每一条都带,且**紧跟标识** —— ⛔ 不是每组一份', () => {
const xs = items();
expect(xs.length).toBeGreaterThanOrEqual(3); // 三组都得有,否则这条等于没测
for (const x of xs) {
expect(Object.keys(x).slice(0, 2)).toEqual(['标识', '开放方式']);
}
// ⚠️ 每条都是**同一句** —— ⛔ 别让谁按 stage 改写出第二个版本
expect(new Set(xs.map((x) => x['开放方式'])).size).toBe(1);
});
test('🔴 例外只放行「一句指令」,⛔ 禁词字面照旧管着它', () => {
const s = String(items()[0]!['开放方式']);
expect(s.length).toBeGreaterThan(0);
for (const w of ['照抄', '原样', '不要说', '绝不', '⛔', '请把', '转述', '必须说']) {
expect(s).not.toContain(w);
}
});
test('⭐ 措辞与 show_guidance 的描述同源,且⛔ 不含工具名', () => {
const s = String(items()[0]!['开放方式']);
expect(s).toContain('开放'); // 工具描述的原词:「把他接下来可以动手的事**开放**给他」
expect(s).toContain('一类'); // 工具描述:「讲完其中**一类**…再讲下**一类**」
// ⛔ 代码标识符不进事实层 —— 进来早晚被念给主管听(沿革见 GUIDANCE_KIND)
expect(s).not.toContain('show_guidance');
});
});
});
/**
......
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