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 问题
- 和产品需求相关的讨论
 
对于不在职责范围内的问题,你应该明确告知用户这不在你的专业领域,并建议用户咨询对应的专业人士。
`

能力边界有两个作用:

  1. 防止越界回答——用户问前端问题时,模型不会硬答
  2. 防止过度发散——用户问一个后端问题时,模型不会顺便把前端、运维、产品需求全讲一遍

没有能力边界的角色设定,模型会倾向于回答所有问题。这看起来像是「服务好」,但实际效果是输出噪声变多、专业度下降。

要素三:行为规则

行为规则是具体的「该做什么、不该做什么」的清单。它和任务描述(第 02 节)的区别是:任务描述是针对单次请求的,行为规则是针对整个对话的。

const prompt = `
你是一个客服对话助手。
 
## 行为规则
1. 不编造产品信息——如果不确定,明确告知用户需要查询后回复
2. 不承诺无法兑现的补偿——退款、换货等操作需要转接人工
3. 不讨论竞品——如果被问到,回答「我可以帮您了解我们产品的功能」
4. 不泄露内部信息——不讨论定价策略、库存数据、其他用户信息
5. 情绪安抚优先——用户表达不满时,先共情再解决问题
`

行为规则的写法有两个要点:

  1. 用具体的场景,不用抽象的原则——「不编造信息」太抽象,「不确定时明确告知用户需要查询后回复」才是可执行的
  2. 覆盖高频的边界情况——客服场景下,用户问竞品、要求赔偿、情绪激动是常见问题。规则要提前覆盖这些情况

要素四:语气风格

语气风格决定了输出的「感觉」——是正式还是随意,是简洁还是详细,是用专业术语还是通俗语言。

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 标题分区有两个好处:

  1. 模型更容易解析——标题形成了结构,模型知道哪部分是身份、哪部分是规则
  2. 人更容易维护——需要修改某个部分时,直接定位到对应的标题

不用 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 里。好处:

  1. 前端不需要知道 system prompt 的内容——只传 role: 'code-reviewer'
  2. 角色配置可以统一管理——改 system prompt 只需要改一个地方
  3. 可以为不同角色选不同的模型和参数——代码审查用 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 里放了动态内容、一个角色做所有事
  • 角色设定是稳定的,任务描述是变化的——两者配合工作

下一篇讲上下文注入——怎么给模型提供背景信息,让它理解你的业务场景。