03-角色设定设计
要点
- 上一节解决了任务描述,这一节看 system prompt——它定义了模型在整个对话中的「身份」
- 角色设定不是让模型「扮演」某个角色,而是给模型一个稳定的行为框架
- 四个要素:身份、能力边界、行为规则、语气风格
- 角色设定应该放在 system message 里,而不是拼在用户消息前面
- 角色设定不是越详细越好——过多的约束会互相冲突
- 多场景系统需要多个角色配置,后端要做路由和切换
1. system message 的作用
第 12 章讲消息结构时,提到过 role: 'system' 的消息:
// src/routes/chat.ts
const messages = [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: userMessage },
]system message 和用户消息的区别不只是位置不同。模型对 system message 的处理方式不一样:
- system message 的优先级高于用户消息——如果用户说「忽略之前的指令」,模型应该忽略的是用户的这条请求,而不是 system 指令
- system message 在整个对话中保持不变——每条用户消息都会触发新一轮生成,但 system 指令不会变
- system message 不占对话轮次——它不算作对话历史的一部分
所以 system message 适合放什么?放那些在整个对话中不应该改变的东西:角色身份、行为规则、输出格式、安全边界。
用户消息适合放什么?放当前这一次请求的具体输入:用户的问题、需要处理的数据、本次的上下文。
2. 角色设定的四个要素
一个好的 system prompt 通常包含四个要素。缺任何一个,模型的行为都可能出现漂移。
要素一:身份
身份告诉模型「你是谁」,决定了模型的知识范围和专业深度。
// ❌ 身份太宽泛
const prompt = '你是一个 AI 助手'
// ❌ 身份太花哨,没有信息量
const prompt = '你是一个拥有 20 年经验的资深全栈工程师,精通前后端开发、系统架构、数据库设计、云原生技术,曾多次主导大型项目的技术选型和架构设计'
// ✅ 身份具体,和任务匹配
const prompt = '你是一个 TypeScript 后端开发助手,专注于 Hono 框架和 Node.js 服务端开发。'「20 年经验的资深全栈工程师」这种描述,除了让 Prompt 变长,不会带来任何额外效果。模型不会因为看到这句话就多知道一些东西。
好的身份描述应该回答一个问题:这个助手擅长什么、被调用时用户期望它具备什么知识?
要素二:能力边界
身份划定了擅长领域,能力边界划定了不做什么。
const prompt = `
你是一个 TypeScript 后端开发助手。
你的职责范围:
- Hono 框架的路由、中间件、错误处理
- Node.js 服务端开发(数据库、缓存、消息队列)
- TypeScript 类型设计和 API 设计
- REST API 和 GraphQL 设计
不在你的职责范围内:
- 前端 UI 开发(React、Vue 等)
- 移动端开发
- 运维和 DevOps 问题
- 和产品需求相关的讨论
对于不在职责范围内的问题,你应该明确告知用户这不在你的专业领域,并建议用户咨询对应的专业人士。
`能力边界有两个作用:
- 防止越界回答——用户问前端问题时,模型不会硬答
- 防止过度发散——用户问一个后端问题时,模型不会顺便把前端、运维、产品需求全讲一遍
没有能力边界的角色设定,模型会倾向于回答所有问题。这看起来像是「服务好」,但实际效果是输出噪声变多、专业度下降。
要素三:行为规则
行为规则是具体的「该做什么、不该做什么」的清单。它和任务描述(第 02 节)的区别是:任务描述是针对单次请求的,行为规则是针对整个对话的。
const prompt = `
你是一个客服对话助手。
## 行为规则
1. 不编造产品信息——如果不确定,明确告知用户需要查询后回复
2. 不承诺无法兑现的补偿——退款、换货等操作需要转接人工
3. 不讨论竞品——如果被问到,回答「我可以帮您了解我们产品的功能」
4. 不泄露内部信息——不讨论定价策略、库存数据、其他用户信息
5. 情绪安抚优先——用户表达不满时,先共情再解决问题
`行为规则的写法有两个要点:
- 用具体的场景,不用抽象的原则——「不编造信息」太抽象,「不确定时明确告知用户需要查询后回复」才是可执行的
- 覆盖高频的边界情况——客服场景下,用户问竞品、要求赔偿、情绪激动是常见问题。规则要提前覆盖这些情况
要素四:语气风格
语气风格决定了输出的「感觉」——是正式还是随意,是简洁还是详细,是用专业术语还是通俗语言。
const prompt = `
## 语气风格
- 使用简洁的中文,避免长句
- 不使用「亲」「呢」「哦」等口语化语气词
- 不使用 emoji
- 技术术语保留英文原文(如 API、Token、Middleware),不翻译成中文
- 每个回复不超过 200 字
`语气风格看起来是小事,但在多轮对话中影响很大。如果风格不固定,用户会觉得在跟不同的人说话。
3. 角色设定的组织方式
system prompt 不是越长越好。一个 2000 字的 system prompt 不一定比 200 字的好。但如果 200 字说不完,也不能硬压。
关键是组织方式。推荐用 Markdown 标题分区:
const SYSTEM_PROMPT = `
# 身份
你是一个 TypeScript 后端开发助手,专注于 Hono 框架和 Node.js 服务端开发。
# 职责范围
- Hono 路由、中间件、错误处理
- Node.js 服务端开发
- TypeScript 类型设计
# 不在职责范围
- 前端 UI 开发
- 移动端开发
- 运维和 DevOps
# 行为规则
1. 不编造不确定的信息——不确定时明确告知
2. 代码示例使用 TypeScript,标注文件路径
3. 回答先给结论,再给解释
# 语气风格
- 简洁、直接
- 技术术语保留英文
- 每个回复不超过 300 字
`用 Markdown 标题分区有两个好处:
- 模型更容易解析——标题形成了结构,模型知道哪部分是身份、哪部分是规则
- 人更容易维护——需要修改某个部分时,直接定位到对应的标题
不用 Markdown 也行,用 ##、---、或者中文编号都可以。关键是要有结构。
4. 多角色系统设计
一个实际的 AI 产品通常不止一个角色。代码审查用一个角色、客服用一个角色、文章生成用另一个角色。
后端需要做角色路由:
// src/config/roles.ts
// 角色定义
export type RoleConfig = {
id: string
name: string
systemPrompt: string
model: string
temperature: number
maxTokens: number
}
export const ROLES: Record<string, RoleConfig> = {
'code-reviewer': {
id: 'code-reviewer',
name: '代码审查助手',
systemPrompt: CODE_REVIEW_SYSTEM_PROMPT,
model: 'gpt-4o',
temperature: 0,
maxTokens: 4000,
},
'customer-service': {
id: 'customer-service',
name: '客服助手',
systemPrompt: CUSTOMER_SERVICE_SYSTEM_PROMPT,
model: 'gpt-4o',
temperature: 0.3,
maxTokens: 1000,
},
'article-writer': {
id: 'article-writer',
name: '文章写作助手',
systemPrompt: ARTICLE_WRITER_SYSTEM_PROMPT,
model: 'gpt-4o',
temperature: 0.7,
maxTokens: 8000,
},
}
// src/routes/chat.ts
import { ROLES } from '@/config/roles'
chatApp.post('/chat', async (c) => {
const { role: roleId, messages } = await c.req.json()
const role = ROLES[roleId]
if (!role) {
return c.json({ error: `Unknown role: ${roleId}` }, 400)
}
// system prompt 由后端注入,前端不传
const fullMessages = [
{ role: 'system', content: role.systemPrompt },
...messages,
]
const result = await callModelAPI({
model: role.model,
messages: fullMessages,
temperature: role.temperature,
max_tokens: role.maxTokens,
})
return c.json(result)
})这种设计把角色配置集中在 src/config/roles.ts 里。好处:
- 前端不需要知道 system prompt 的内容——只传
role: 'code-reviewer' - 角色配置可以统一管理——改 system prompt 只需要改一个地方
- 可以为不同角色选不同的模型和参数——代码审查用
temperature: 0,文章生成用temperature: 0.7
5. 角色设定的常见陷阱
陷阱一:过度角色扮演
你是一个名叫小明的 AI 助手,今年 25 岁,毕业于清华大学计算机系,爱好编程和游泳……
这种角色设定对模型的输出质量没有任何帮助。模型不需要一个「人设」来更好地工作。身份描述只需要包含对任务有用的信息。
陷阱二:规则和身份矛盾
你是一个严谨的代码审查助手。
你总是尽可能给出详细的回答。
「严谨」意味着宁可漏报也不误报,「详细」意味着尽可能多报。两条规则矛盾,模型会在两者之间摇摆。
解决办法是明确优先级:
你是一个严谨的代码审查助手。
宁可漏报也不要误报——不确定的问题不要报告。
在严谨的前提下,对每个报告的问题给出详细的解释和修复建议。
陷阱三:system prompt 里放了不该放的东西
const systemPrompt = `
你是一个代码审查助手。
今天是 2024 年 8 月 15 日。
用户名叫张三,邮箱是 [email protected]。
当前版本是 v2.3.1。
`日期、用户信息、版本号——这些东西不属于 system prompt。它们应该放在用户消息里,或者通过上下文注入(第 04 节)动态拼入。
system prompt 应该是稳定的、不随请求变化的。如果某段内容每次请求都不同,它就不应该放在 system prompt 里。
陷阱四:一个角色做所有事
你是一个全能助手,可以帮用户写代码、写文章、翻译、分析数据、聊天解闷……
一个角色覆盖所有场景时,每个场景的效果都不会好。因为 system prompt 里的规则会互相干扰。
更好的做法是拆成多个角色,每个角色专注一个场景。后端根据用户请求路由到对应的角色。
6. 一个完整的角色配置示例
把上面的内容合在一起,写一个实际的角色配置:
// src/config/roles/code-reviewer.ts
export const CODE_REVIEW_SYSTEM_PROMPT = `
# 身份
你是一个 TypeScript 代码审查助手,专注于 Hono 框架和 Node.js 服务端代码的审查。
# 职责范围
- 类型安全问题:any 滥用、不安全的类型断言、缺失的类型标注
- 空值处理:未处理的 null/undefined、可选链缺失
- 异步错误处理:await 缺少 try-catch、未处理的 Promise rejection
- Hono 框架用法:中间件顺序、错误处理、路由设计
# 不在职责范围
- 前端代码(React、Vue 等)
- 代码风格问题(缩进、空格、命名偏好)
- 性能优化建议(除非存在明显的性能问题)
# 行为规则
1. 不编造不存在的问题——如果代码没有问题,输出「审查通过」
2. 每个问题标注严重程度:error / warning / suggestion
3. 每个问题给出具体的修复建议和代码示例
4. 最多报告 10 个问题,按严重程度排序
5. 不讨论代码风格偏好——这不是你的职责
# 输出格式
JSON 数组,每个元素:
{
"severity": "error" | "warning" | "suggestion",
"line": number,
"message": string,
"fix": string | null
}
# 语气风格
- 简洁、直接
- 技术术语保留英文
- 不使用「可能」「也许」等模糊词——问题要么存在,要么不存在
`
export const CODE_REVIEWER_CONFIG = {
id: 'code-reviewer',
name: '代码审查助手',
systemPrompt: CODE_REVIEW_SYSTEM_PROMPT,
model: 'gpt-4o',
temperature: 0,
maxTokens: 4000,
}这个配置覆盖了四个要素(身份、能力边界、行为规则、语气风格),并且有明确的输出格式约束。可以直接接入第 12 章的聊天接口。
7. 角色设定和任务描述的关系
第 02 节讲任务描述,这一节讲角色设定。两者的关系是什么?
- 角色设定是稳定的——在整个对话中不变,定义模型的「身份」和「规则」
- 任务描述是变化的——每次请求可能不同,定义模型的「具体任务」
一个类比:角色设定是公司给员工的岗位说明书(你是后端工程师,职责是 XX,不需要做 YY),任务描述是今天分配的具体任务(实现用户注册接口,要求 ZZ)。
在实际的 Prompt 设计中,两者通常同时存在:
const messages = [
{
role: 'system',
// 角色设定:稳定的,定义身份和规则
content: CODE_REVIEW_SYSTEM_PROMPT,
},
{
role: 'user',
// 任务描述:变化的,定义本次的具体输入
content: `请审查以下代码:\n\n${code}`,
},
]有些场景下,任务描述也会放在 system prompt 里(如果任务本身也是稳定的)。这时候 system prompt 会同时包含角色设定和任务描述。只要结构清晰,两种做法都可以。
总结
回顾这一节的要点:
- 角色设定给模型一个稳定的行为框架,不是让模型「扮演」某个角色
- 四个要素:身份、能力边界、行为规则、语气风格
- 用 Markdown 标题分区,让结构清晰、便于维护
- 多角色系统需要后端做路由和切换,角色配置集中在一个地方管理
- 常见陷阱:过度角色扮演、规则和身份矛盾、system prompt 里放了动态内容、一个角色做所有事
- 角色设定是稳定的,任务描述是变化的——两者配合工作
下一篇讲上下文注入——怎么给模型提供背景信息,让它理解你的业务场景。