多角色智能体人格模板:分层回落与系统提示词组装实践

发布时间:2026/10/29 12:09:33
多角色智能体人格模板:分层回落与系统提示词组装实践

做多角色智能体的人大概都遇到过这种局面:同一个角色,昨天回答得规规矩矩,换个场景就开始跑偏;提示词散落在配置文件、业务代码和数据库三处,想改一句行为约束得翻遍五个文件;最要命的是某个角色配置漏了一个字段,程序一声不响,模型直接开始自由发挥,事后复盘才发现模板压根没加载上。这几个月我把 OpenHanako 的人格模板体系从头到尾拆了一遍,从 agents-templates 目录的字段约定,到系统提示词最终的拼装顺序,再到那条最容易被忽略的回落链,基本摸清了它为什么这么设计、哪些地方能改、哪些地方千万别碰。这篇文章按目录约定、回落逻辑、组装原理、实操验证、故障排查的顺序往下讲,能直接抄的配置、能复现的排查路径、还有我踩过的几个坑都写在里面。读者有一点 LLM 应用开发基础最好,完全没接触过人格模板也能跟得上,因为我会从最朴素的“为什么要分层”讲起。

1. 人格模板体系到底在解决什么问题

1.1 单文件提示词的三个死结

刚开始做角色化智能体的时候,绝大多数人的做法是写一个大字符串,把所有身份设定、行为规范、输出格式、安全底线全塞在一起,长度动辄两三千字。这种做法在单角色、单场景的 Demo 里完全够用,可一旦角色数量上到五个以上、场景开始细分,问题就集中爆发了。

第一个死结是角色漂移不可控。同一个角色在闲聊场景和审核场景里的表现应该不一样,但因为只有一份提示词,你要么写得很宽泛(结果两边都不像),要么在调用时临时拼字符串(结果拼出来的东西没人知道长什么样)。第二个死结是改动影响面完全不可预判。产品经理说客服角色语气太生硬,你改了语气那一句,结果审核角色的判定标准跟着松了,因为它们在同一个文件里。第三个死结是复用和测试无从下手。你想给输出格式做一次回归测试,可格式约束和角色身份揉在一起,根本没法单独抽出来验证。

这三个死结的共同根源其实是一个:把不变量和变量混在同一个容器里。全局的语气底线、安全约束、输出格式,这些对所有角色都成立,属于不变量;而角色身份、专业领域、场景增量,这些每个角色都不一样,属于变量。混在一起写,就等于放弃了让系统替你管理差异的可能。

1.2 分层拼装带来的确定性

OpenHanako 的做法很朴素,就是把上面说的不变量和变量拆开,用目录结构和字段约定强制分层。base 层放全局底线,包括语气基调、拒答原则、输出格式骨架;agent 层放角色身份和专业边界;scene 层放具体场景的增量指令。

这么拆之后,最直接的收益是改动变得可预期。改 base 层的输出格式,所有角色同步生效,你知道影响面是全部;改 hanako 这个角色下的 scene/review.yaml,只有审核场景受影响,其他场景纹丝不动。这种“影响面显式化”的价值,在角色数量超过十个之后会体现得极其明显。

第二个收益是可以分层测试。你可以固定 agent 层不变,只替换 scene 层跑一遍回归,看行为差异是不是完全由场景增量引起。这在排查“为什么同一个问题在两个场景下回答差这么多”的时候特别有用。

第三个收益藏在细节里:模板变成了可以 diff 的资产。人格不再是一段神秘的大字符串,而是一组有名字的文件。谁在什么时候改了哪条规则,版本控制里清清楚楚。这一点在多人协作的项目里,比任何技术优化都值钱。

1.3 回落链的本质是缺省值传播

很多人第一次看到“回落链”这个词,会下意识理解成继承树,觉得 scene 继承 agent、agent 继承 base,是一棵父子关系的树。这个理解方向对,但不够准确,容易在实操里踩坑。

回落链更准确的描述是链式查找加缺省值传播。当系统需要某个 block 的内容时,它按固定顺序往上找:当前场景有没有定义?有就用;没有就找当前角色的定义;角色也没定义,就找组织级 base;base 还是没有,就用框架内置的兜底。找到即止,不再往上。这个模型和 CSS 的层叠规则很像,也和配置中心的优先级查找很像——它不是“子类覆盖父类方法”那种面向对象语义,而是“就近取值”的查找语义。

这个区别非常关键。继承语义下,子层会完整拿到父层的所有内容再做增量修改;查找语义下,子层只拿到自己需要的、且上层确实提供了的那部分。后者更轻,但也更容易出现“我以为它继承了,其实上层根本没写”的情况。我在第一次接入的时候就栽在这上面,某个角色没写输出格式,我以为会从 base 拿到,结果 base 里那一项是空的,最后模型输出格式五花八门,查了半天才发现是回落链走到了框架兜底那一层。

2. agents-templates 的目录约定与字段职责

2.1 一个角色的目录长什么样

先看结构,结构看懂了,后面所有逻辑都好推。这是一个典型的角色目录形态:

agents-templates/ ├── _base/ │ ├── identity.md # 全局身份基调与语气底线 │ ├── guardrails.md # 安全与拒答原则,永不裁剪 │ └── output-format.md # 输出结构骨架 ├── hanako/ │ ├── agent.yaml # 元数据、回落配置、预算、版本 │ ├── persona.md # 角色身份、知识边界、表达习惯 │ ├── rules.md # 行为规则条目 │ ├── examples.yaml # 少样本示例,按场景分组 │ └── scenes/ │ ├── review.yaml # 审核场景增量 │ └── explain.yaml # 讲解场景增量 └── _framework/ └── defaults.yaml # 框架级兜底,一般不要动

这里有几个设计取舍值得说清楚。persona.md 和 rules.md 分开,是因为它们的生命周期不一样:persona 属于角色定义,一旦定下来很少改;rules 属于调优对象,上线后几乎每周都在动。放在两个文件里,diff 的时候一眼就能看出这次改动是“角色定位变了”还是“规则微调”。scenes 做成独立目录,是为了让场景可以独立增删,不用改主配置文件。新增一个场景只需要丢一个 yaml 进去,回落链会自动把它接上。

_base和_framework用下划线前缀,这是个约定俗成的做法,用来标记“非角色目录”,扫描器遍历时会跳过它们,避免被误当成角色加载。这个细节看起来无关紧要,但我在一次批量导入里就吃过亏——扫描器把_base当成一个叫_base的角色导进去了,调用时报了一堆找不到 scene 的错。

2.2 agent.yaml 里的关键字段

元数据文件是整个组装过程的入口,字段不多但每一个都有明确职责,写错一个就可能导致回落链走偏:

字段作用常见取值写错的后果
name角色唯一标识,也是回落链查找的 key小写短横线命名大小写不一致导致找不到角色
extends显式声明父级模板角色名或_base写成不存在的名字,回落中断
fallback未命中时的兜底角色通常指向_base留空则直接落到框架默认
token_budget该角色系统提示词的长度上限2000 到 4000设太小会裁掉关键规则
required_slots渲染前必须存在的变量数组,如topic、audience漏填会导致占位符残留
cache_key缓存标识的组成方式模板 hash 加变量 hash含时间戳会击穿缓存
version模板版本号语义化版本无版本则无法灰度回滚

extends和fallback这两个字段最容易混。简单说,extends是声明式地从谁那里拿默认值,是静态关系,写死在配置里;fallback是运行时找不到时的应急出口,是动态兜底。前者决定正常路径,后者决定异常路径。我在一次重构里把两者混用,结果正常角色也走了应急兜底,语气全变了,排查了两个小时才定位到是fallback写得过于激进。

2.3 为什么用 Markdown 存人格而不用纯 JSON

有人会问,既然要结构化,为什么不干脆全用 JSON 或者 YAML。答案在维护者身上。人格文本的主要编辑者往往不是写代码的人,可能是做内容策略的、做运营的,让他们在 JSON 字符串里写大段中文,转义符和换行符能把人逼疯,而且 diff 出来一片红绿根本看不出改了哪句话。

Markdown 的好处是长文本可读、diff 友好、编辑器支持好。写一段“你是一个耐心的讲解者,遇到概念会先给类比再给定义”这样的内容,在 md 里就是一行自然文字,在 JSON 里得包一层引号加转义。而结构化字段,比如预算、回落、必填变量,这些本来就是机器读的,放 YAML 里最合适。所以最终的形态是YAML 管结构,Markdown 管内容,各取所长。

注意:Markdown 文件里不要写 HTML 注释来“临时禁用”某段内容。有些渲染管线会把注释原样带进系统提示词,模型看得见。真要禁用,就挪到另一个文件或者用配置开关控制。

2.4 命名与版本:一个不起眼但致命的细节

命名规范这件事,不出问题的时候没人关心,出了问题就是灾难。我在项目里定下的规则是:角色名一律小写加短横线,场景名跟着角色名走,不加前缀。hanako/scenes/review.yaml对应的场景标识就是review,不写hanako_review也不写review_v2。理由很简单,回落链查找是按“角色名 + 场景名”两级拼出来的,如果场景名里再嵌角色名,拼接时就会重复,查找直接失败。

版本号的作用不是好看,是为了回滚。人格模板这种东西,改一条规则可能导致整个角色的行为风格变化,而效果好坏往往要跑几天数据才能看出来。没有版本号,出问题时你连“退回上一版”这个动作都做不了。我的做法是每次改动都递增 patch 版本,重大调整递增 minor,每次发布记录里写清楚“改了哪条规则、预期影响什么、验证方式是什么”。这份记录在三个月后回头看,价值远超写它花的那十分钟。

3. 回落链:四层优先级怎么算

3.1 四层的定义与查找顺序

完整的回落链是四层,从高到低依次是:

优先级层级存放位置典型内容可变频率
1场景层scenes/*.yaml场景专属指令、输出格式微调高
2角色层persona.md、rules.md身份、知识边界、行为规则中
3组织层_base/*.md全局语气、安全底线、格式骨架低
4框架层_framework/defaults.yaml最终兜底,保证渲染不失败极低

查找发生在每一个 block 上,不是整个提示词上。这一点前面提过,这里再强调一次,因为它是整个体系的心脏。系统会先算出这次渲染需要哪些 block,比如 identity、guardrails、output-format、scene-instruction、examples 这五个,然后对每一个 block 单独走一遍四层查找,谁先命中就用谁。所以完全可能出现这种情况:identity 来自角色层,output-format 来自组织层,scene-instruction 来自场景层,examples 来自框架兜底。这在一份完整提示词里非常正常。

3.2 逐 block 回落而不是整文件回落

为什么坚持按 block 而不是按文件回落?因为按文件回落会导致大量重复。假设按文件,场景层只要想加一句“审核时优先指出事实错误”,就得把整个角色层的规则文件抄一遍再改,抄的那一刻起,两份文件就开始各自演化,三个月后彻底不同步。

按 block 回落则允许最小增量。场景层只写它真正要改的那一块,其余全部自动往下取。这样场景文件往往只有二三十行,改动意图一目了然。代价是调试时需要工具支持,你得能随时打印出“这一次渲染,每个 block 到底来自哪一层”。这个能力后面会单独讲,是排查问题的第一把钥匙。

3.3 合并策略:覆盖、追加、前插

光有回落还不够,因为有些 block 不是简单的“有没有”问题,而是“要不要在原有基础上加东西”。所以每个 block 在配置里可以带一个合并模式标记:

  • replace:完全覆盖下层内容,默认行为,适用于输出格式、身份定义这类必须唯一的块;
  • append:追加在下层内容之后,适用于场景额外规则、补充禁忌;
  • prepend:插入在下层内容之前,适用于需要提升优先级的前置指令。

这三种模式的组合能覆盖绝大多数需求。比如审核场景要在角色原有规则基础上再加三条判定标准,就用append;讲解场景要把输出格式整个换成“先结论后展开”,就用replace。我个人的经验是,能用 append 就别用 replace,因为 append 保留了上下文的连续性,模型对“原有规则加补充”的响应通常比“整段换掉”更稳定。replace 用多了,角色会觉得自相矛盾,因为它在不同场景下接收到的身份描述差异太大。

3.4 冲突消解与循环依赖检测

回落链最怕的是成环。A 的extends指向 B,B 的extends指向 A,查找时就会无限递归。框架层面的处理方式是带访问标记的深度优先遍历:进来先给当前节点打标记,如果遍历过程中撞到已标记节点,直接判定成环并抛出明确错误,而不是静默返回空值。同时设一个最大深度,比如 8 层,超过就截断并告警。

冲突消解则遵循一个原则:同层内按声明顺序,后者覆盖前者;跨层按优先级,高层覆盖低层。这条规则听起来简单,但实操中最容易出问题的是“同层内顺序”这一条。YAML 里的 key 顺序在大多数解析器里是保留的,但如果你用字典合并的方式处理,顺序可能丢失。我的做法是显式给每个 block 一个整数order字段,合并前先按 order 排序,不依赖解析器的隐式行为。这个改动只花了二十分钟,但省掉了我后面无数次“为什么这次规则生效顺序不一样”的困惑。

4. 组装:从模板到最终系统提示词

4.1 固定的块顺序及其背后的注意力逻辑

回落链决定了“每个块用哪份内容”,组装则决定“这些块按什么顺序拼起来”。OpenHanako 的块顺序是固定的:

  1. 身份定义(你是谁)
  2. 能力边界(你会什么、不会什么)
  3. 行为规则(你怎么做)
  4. 输出格式(你怎么说)
  5. 场景上下文(这次具体做什么)
  6. 少样本示例(照着这个来)

这个顺序不是随便排的。大模型对提示词的开头和结尾注意力最强,中间部分容易被稀释,这是位置偏差的常见表现。所以最不能丢的东西必须放两头:身份和边界放最前面,因为一旦角色定位错了,后面全错;示例放最后,因为它离当前生成位置最近,约束力最强。

行为规则和输出格式放中间,是因为它们属于“软约束”,即使部分稀释,影响也可控。而场景上下文放在示例之前,是为了让示例成为“场景下的示范”,而不是孤立的样例。这个顺序我调过两版,第一版把示例放在身份后面,结果模型老是模仿示例的具体内容而不是抽象风格;挪到最后之后,模仿的对象变成了风格,效果明显改善。

注意:安全与拒答类内容最好在开头和结尾各出现一次,形成首尾呼应。中间位置的安全约束在实际测试里衰减得比想象中快,尤其是长上下文场景。

4.2 变量插值与占位符的兜底

模板里的变量用双花括号标记,比如{{topic}}、{{audience}}、{{tone}}。渲染前会做两件事:一是检查required_slots里声明的变量是否都存在,缺任何一个就直接报错,不进入渲染;二是对非必填变量做兜底替换,缺失时用预置的默认值填充,同时记一条日志。

这个设计的意图很明确:宁可渲染失败,也不要渲染出一份带残留占位符的提示词。我见过最隐蔽的 bug 就是占位符没替换,模型看到{{topic}}这种字面量,要么照抄进回答,要么自己脑补一个话题,两种结果都很难排查,因为日志里一切正常。所以务必把必填校验做成硬中断。

兜底值也要讲究。不要用“未知”“无”这种词,模型看到“未知”很容易主动去追问用户,破坏对话节奏。更好的做法是给一个语义中性的默认值,比如topic缺失时默认成“当前对话主题”,让模型顺着上下文自己判断,而不是追问。

4.3 token 预算与裁剪顺序

系统提示词不是越长越好。经验上,超过 3000 到 4000 token 之后,新增规则的边际效用急剧下降,甚至开始互相干扰。所以每个角色都要设token_budget,渲染完成后如果超了,按固定顺序裁剪。

裁剪顺序从“最可牺牲”到“最不可牺牲”:

  1. 先裁示例数量,从后往前删,保留最贴近当前场景的两到三条;
  2. 再裁场景上下文里的补充说明,只留核心指令;
  3. 再裁行为规则里的解释性文字,保留规则条目本身;
  4. 最后才动角色身份描述,且只压缩修饰语,不删核心定位;
  5. 安全与拒答内容永不裁剪。

token 估算有个粗略经验值:中文大约一个汉字折 0.7 到 1 个 token,英文大约四个字符折一个 token。按这个估,一份 2500 字的中文系统提示词大概在 2000 到 2500 token 之间。这个估算不精确,但用来做预算控制足够了。真要精确,就在渲染后调一次分词接口数一下,把结果缓存起来。

举个具体例子:模型上下文窗口 32k,预留 8k 给对话历史,4k 给输出,剩下 20k 里系统提示词最多给 4k。按中文估算,就是 4000 到 5000 字的上限。如果你的角色模板写到 6000 字,那就必须裁剪,否则对话稍微长一点就会顶掉历史。

4.4 组装结果长什么样

渲染完成后,最终交给模型的系统提示词大概是这样一段结构(节选):

你是 hanako,一名负责内容审核与事实核查的助手。 你的能力边界:你可以判断事实准确性、指出逻辑漏洞、标注不确定信息。 你不提供法律、医疗等专业领域的最终结论。 行为规则: 1. 先给判断,再给依据。 2. 依据必须指向原文具体位置。 3. 无法确认时明确说明,不猜测。 输出格式: - 结论:(通过 / 需修改 / 无法判断) - 依据:(逐条列出) - 建议:(可选) 当前场景:对一段用户提交的说明文字做事实核查。 待核查内容:{{content}} 示例: 输入:…… 输出:……

看得出,每个块都带着来源层的痕迹:身份来自角色层,能力边界来自角色层,行为规则来自角色层加场景追加,输出格式来自组织层被场景覆盖,示例来自角色层。这种“每块都可能来自不同层”的形态,就是回落链的直接产物。

5. 实操:跑通一次完整的组装

5.1 最小可运行实现

把上面的逻辑落成代码,核心就是一个“逐 block 查找加合并”的函数。下面这段是简化版,去掉缓存和日志,只保留主干,方便理解:

def resolve_block(block_name, agent, scene, base, framework): """按四层优先级查找某个 block 的内容""" layers = [ (scene, "scene"), (agent, "agent"), (base, "org"), (framework, "framework"), ] for layer, source in layers: node = layer.get(block_name) if node and node.get("content"): return { "content": node["content"], "merge": node.get("merge", "replace"), "source": source, } return {"content": "", "merge": "replace", "source": "none"} def assemble(agent_name, scene_name, slots): agent = load_agent(agent_name) scene = load_scene(agent_name, scene_name) base = load_base() framework = load_framework() # 必填校验,缺一个就直接失败 for slot in agent.get("required_slots", []): if slot not in slots: raise ValueError(f"missing required slot: {slot}") order = ["identity", "capability", "rules", "output_format", "scene_context", "examples"] blocks = {} for name in order: blocks[name] = resolve_block(name, agent, scene, base, framework) # 合并同 block 的多层内容 merged = merge_by_mode(blocks) # 变量插值,缺失的走默认值 rendered = render_template(merged, slots) # token 预算裁剪 rendered = trim_to_budget(rendered, agent["token_budget"]) return rendered

这段代码里有两个地方值得多看两眼。resolve_block返回的不只是内容,还有来源标记,这个标记在调试时极其重要,后面会讲怎么用。merge_by_mode处理的是同一个 block 在不同层都出现时的合并,比如规则在角色层和场景层都写了,那就按各自的 merge 模式拼起来。

5.2 三个层面的验证

模板写完,不能凭感觉说“应该好了”,得按层验证。

字段层验证:确认每个角色目录下的必填字段齐全,name、extends、token_budget、required_slots一个不缺,extends指向的目标真实存在。这一步可以写个静态检查脚本,遍历所有角色跑一遍,几秒钟出结果。我一般把它挂在提交前,能挡掉八成低级错误。

渲染层验证:用几组典型变量跑一遍完整组装,检查三件事——渲染结果里有没有残留的{{ }},长度有没有超预算,每个 block 的来源标记是不是符合预期。第三件事最有用,你可以直接看出“这个格式到底是从哪一层来的”,比翻代码快得多。

行为层验证:拿一组固定的测试输入,跑模型,看输出是否符合预期。这一步没法自动化到百分百,但可以做半自动打分。我的做法是准备二十条覆盖典型场景的输入,人工标注期望行为特征,然后每次改动后跑一遍,比对差异。二十条不多,但足以发现“规则冲突导致行为摇摆”这类问题。

5.3 一次对照实验的记录

有一次我给审核角色加了一条“优先指出事实错误”的规则,加完后感觉整体变好了,但说不清好在哪里。为了确认这条规则真的有用而不是心理作用,我做了一次对照:同一批二十条输入,一组带这条规则,一组不带,各跑三次,人工打分。

结果挺意外:带规则的那组在“是否指出事实错误”这一项上明显更好,但在“是否给出修改建议”上反而下降了。原因是这条规则占据了注意力,模型把精力放在找错上,忘了给建议。后来我把规则改成“先指出事实错误,再给出至少一条修改建议”,两项指标才一起上来。

这个实验教给我一个道理:规则是有成本的,加一条就可能挤掉另一条。所以每次加规则,都要问一句“它会不会挤掉什么”。规则总数控制在二十条以内比较稳妥,超过三十条之后互相干扰的概率显著上升。

6. 常见问题与排查实录

6.1 六类高频故障速查

实际跑下来,问题集中在下面这几类,整理成表方便对着查:

现象可能原因定位手法处理方式
角色语气突然变了回落链走到了框架兜底打印 block 来源标记补全对应层的定义
输出格式五花八门格式块用了 replace 且场景层写空检查场景 yaml 的 merge 模式改用 append 或补内容
提示词里出现{{xxx}}必填校验被绕过搜索渲染结果中的双花括号补齐必填校验逻辑
模型行为僵硬、过度拒绝规则里绝对化措辞堆太多统计“必须/绝对”出现次数换成条件式表述
场景指令似乎没生效被后面的示例冲淡了调整块顺序,示例后移场景指令紧跟格式块
响应变慢、成本上升系统提示词超预算未裁剪统计渲染后 token 数收紧预算并检查裁剪顺序

表格里第五行值得单独说。场景指令没生效,很多时候不是没加载,而是位置不对。如果场景指令夹在一大段示例前面,模型很容易被示例带跑,把场景的特殊要求忽略了。把场景指令挪到示例之前、紧跟输出格式之后,命中率会明显提升。这个调整我做过三次,每次都有改善。

6.2 二分法定位:最土但最有效

遇到“角色行为不对”这类模糊问题,最有效的办法不是猜,是二分。具体做法:先把当前完整的系统提示词打印出来,然后砍掉一半内容(比如只留前三个块),跑同样的输入,看行为是否还异常。如果异常消失,说明问题在被砍掉的那一半里;如果还在,继续砍。通常三到四轮就能锁定到具体某一段。

这个办法土,但它绕过了所有“我以为”的假设。我排查过一个持续两天的诡异问题:某角色在长对话后期会突然开始用英文回答。查配置、查变量、查缓存都没问题,最后二分到示例块——示例里有一条中英混排的样例,短对话时影响不大,长对话时模型被那条例子的语言风格带偏了。删掉重写那条示例,问题消失。

用二分法有个前提:你得能稳定复现问题。所以测试输入要固定,不能每次换一批。我的习惯是维护一个regression/目录,里面放固定输入和期望特征,出问题先跑这个。

6.3 我的避坑清单

几条从实际踩坑里总结出来的,写在这里省得别人再走一遍:

  • 不要在模板里写“你是一个 AI 助手”这类冗余身份句。角色身份已经定义了,再叠一层通用身份会让模型在两套自我认知之间摇摆,表现为时而像个角色,时而像个通用助手。
  • 规则的措辞要具体到可执行。“保持专业”这种话模型没法执行,“不使用感叹号、不使用第一人称、每段不超过三句”就能执行。抽象形容词在提示词里基本等于噪声。
  • 示例的质量比数量重要十倍。三条精心挑选的示例胜过十条随手凑的。凑数的示例会引入噪声风格,反而拉低一致性。
  • 场景文件不要超过五十行。超过说明你在场景层做了本该在角色层做的事,这时候应该回头重构,而不是继续加。
  • 改完模板一定要清缓存。模板 hash 变了但缓存 key 没变,是最典型的“改了没生效”原因,而且极难自查。
  • 给每个 block 加来源标记,并在日志里输出。这个投入半天,回报是后面所有排查都快一倍。

提示:调试期可以在渲染结果末尾临时追加一段“来源清单”,列出每个 block 来自哪一层。上线前记得关掉,避免占用预算。

7. 模板体系的长期维护

7.1 模板是长出来的,不是设计出来的

一开始我想把整个体系一次性设计到位,结果发现根本做不到。真正好用的模板结构,是在用了两三个月、踩了十几个坑之后慢慢收敛出来的。最初的版本只有 persona 一个文件,后来因为规则改得太频繁才拆出 rules.md;最早没有 scenes 目录,是因为只有两个场景,直接写在调用代码里,直到场景涨到六个、代码里全是 if-else 才重构出来。

所以我的建议是:别追求一步到位,先把回落链的骨架搭起来,具体分几层、每层放什么,跟着实际需求长。骨架对了,后面加内容都是增量;骨架错了,内容越多越难改。

7.2 回归测试集比模板本身更值钱

模板会一直变,但测试集是相对稳定的资产。我现在的做法是每发现一个新问题,就把它固化成一条测试用例,放进回归集。半年下来攒了六十多条,覆盖了语气漂移、格式错乱、场景串味、拒答过度这几类典型问题。每次改模板先跑一遍,能挡掉大部分回归。

这套东西的价值在于,它把“感觉变好了”变成“这几项指标确实变好了,那几项没退步”。人格模板调优最怕的就是凭感觉,而感觉这东西,改完第二天就不准了。

最后分享一个小技巧:给每个角色维护一份变更日志,不写在代码里,就写在角色目录下一个CHANGELOG.md里,每次改动记一行——改了什么、为什么改、验证结果如何。这东西看起来像形式主义,但当你三个月后想回滚某个改动、却想不起当初为什么加它的时候,这行字能救你一命。我自己就靠它找回过一条被误删的关键规则,那条规则当初加的时候只写了半句话,但足够让我想起它的来龙去脉。