02-任务描述设计
要点
- 上一节确立了五条原则,这一节开始拆第一条:明确任务
- 任务描述的核心问题是:模型读完这段话之后,知不知道该做什么、做到什么程度
- 模糊的任务会让模型自行脑补——脑补的方向不可控
- 三个层次:动词要可执行、对象要有边界、结果要可验证
- 复杂任务需要拆解成步骤,但拆解方式会影响模型的行为
- 任务描述不是一次写定的——它需要跟着测试集里的失败案例一起迭代
1. 一个典型的问题
先看一个实际场景。后端要做一个「智能客服摘要」功能:把一段客服对话总结成一条简短的摘要,存到数据库里供后续检索。
第一版 Prompt 可能是这样的:
// src/services/prompt/chat-summary.ts
const CHAT_SUMMARY_PROMPT = `
请总结以下客服对话。
`这个 Prompt 的问题在哪?
- 「总结」是一个开放动作——模型不知道你要 20 字还是 200 字,不知道重点放在用户诉求还是处理结果,不知道用什么人称
- 没有指定输出长度,模型可能输出 300 字
- 没有指定关注重点,模型可能关注了对话中的闲聊部分
- 没有指定输出格式,可能是散文、可能是列表
换一版具体一点的:
const CHAT_SUMMARY_PROMPT = `
你是一个客服对话摘要助手。
输入:一段客服和用户的对话记录。
任务:用一句话总结用户的核心诉求和处理结果。
要求:
- 长度不超过 50 字
- 格式:「用户反馈{问题},{处理结果}」
- 如果问题未解决,处理结果写「待处理」
- 不要包含客服的寒暄和流程性话语
`两版 Prompt 的差别不在于字数,而在于「模型读完之后,是否知道该做什么、做到什么程度」。第二版做到了。
2. 任务描述的三个层次
把任务描述的质量拆开看,可以分成三个层次,每个层次解决一个问题。
第一层:动词要可执行
模型需要知道该执行什么动作。「分析」「处理」「看看」这类动词太宽泛。换成具体的动作词:
| 模糊动词 ❌ | 可执行动词 ✅ |
|---|---|
| 分析 | 提取、分类、排序、对比、校验 |
| 处理 | 翻译、改写、补全、格式化、脱敏 |
| 看看 | 检查、验证、识别、匹配 |
| 优化 | 精简(到 N 字以内)、扩展(补充三个示例)、重组(按时间排序) |
来看一个对比:
// 模糊 ❌ — 「优化」是什么意思?
const prompt = '优化这段产品描述'
// 可执行 ✅ — 模型知道该做什么
const prompt = '将以下产品描述精简到 100 字以内,保留核心卖点,去掉修饰性形容词。'第二层:对象要有边界
模型需要知道操作的对象是什么、范围在哪里。
// 对象不明确 ❌ — 「关键字段」是哪些?
const prompt = '提取订单中的关键字段'
// 对象明确 ✅ — 字段列表固定,模型不会自由发挥
const prompt = `
从以下 JSON 订单数据中提取这些字段:
- orderId:订单号
- userId:用户 ID
- totalAmount:总金额(分)
- status:订单状态
- createdAt:创建时间
以 JSON 对象输出,只包含以上五个字段。
`对象没有边界时,模型会按自己的理解选。不同次调用可能选不同的字段,下游代码就炸了。
第三层:结果要可验证
模型输出完之后,你怎么判断它对不对?如果连你自己都说不清期望的输出是什么,模型更说不清。
// 不可验证 ❌ — 「合适」是什么标准?
const prompt = '给这段代码起一个合适的函数名'
// 可验证 ✅ — 有明确的命名规范可以对照
const prompt = `
根据以下命名规范,给函数起名:
- 动词开头(get/set/create/delete/update/check)
- camelCase 格式
- 名字要体现输入和输出
输入:一个用户 ID,查询该用户的所有订单
输出:订单数组
函数名:
`「可验证」不意味着「唯一正确答案」,而是说「有一个标准可以判断输出是否合格」。命名规范就是一个标准——符合规范的通过,不符合的不通过。
3. 复杂任务的拆解
单个动作能描述清楚的任务不多。大部分实际业务需要多个步骤。这时候需要把任务拆解成子任务。
拆解方式会直接影响模型的执行效果。两种常见的拆解思路:
思路一:按步骤顺序拆
把任务拆成一个线性的步骤链,每一步依赖上一步的输出。
// src/services/prompt/article-writer.ts
const ARTICLE_WRITER_PROMPT = `
根据用户提供的主题,撰写一篇技术博客。按以下步骤执行:
步骤 1:列出 3-5 个候选标题
步骤 2:从候选标题中选择最适合的一个
步骤 3:根据选定的标题,写出文章大纲(3-5 个章节)
步骤 4:根据大纲,撰写每个章节的内容
步骤 5:检查全文,修正语法错误和不连贯的地方
每个步骤的输出格式:
步骤 1 输出:标题列表,每行一个
步骤 2 输出:选定的标题
步骤 3 输出:大纲,用 - 列出章节
步骤 4 输出:完整文章内容(Markdown 格式)
步骤 5 输出:修正后的完整文章内容
最终只输出步骤 5 的结果。
`这种拆解适合步骤之间有严格依赖关系的场景。但有一个问题:模型在同一个回复里要执行所有步骤,中间步骤的输出会占用上下文窗口,而且模型可能跳过某些步骤。
思路二:按职责拆,每步独立调用
把每个子任务拆成独立的 Prompt,由后端代码控制调用链。
// src/services/prompt/article-writer-pipeline.ts
import { callModelAPI } from '@/lib/llm'
// 每个步骤一个独立的 Prompt 函数
async function generateTitles(topic: string): Promise<string[]> {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '根据用户提供的主题,生成 5 个技术博客标题。每行一个标题。',
},
{ role: 'user', content: topic },
],
temperature: 0.8,
})
return response.choices[0].message.content.split('\n').filter(Boolean)
}
async function selectBestTitle(titles: string[]): Promise<string> {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '从以下标题中选择最适合技术博客的一个。只输出标题文本,不要解释。',
},
{ role: 'user', content: titles.join('\n') },
],
temperature: 0,
})
return response.choices[0].message.content.trim()
}
async function generateOutline(title: string): Promise<string[]> {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '根据文章标题,生成 3-5 个章节标题。每行一个章节。',
},
{ role: 'user', content: title },
],
temperature: 0.3,
})
return response.choices[0].message.content.split('\n').filter(Boolean)
}
async function writeSection(title: string, section: string): Promise<string> {
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `你正在撰写一篇标题为「${title}」的技术博客。请撰写「${section}」章节的内容。800-1200 字,包含代码示例。Markdown 格式。`,
},
{ role: 'user', content: `请撰写「${section}」章节` },
],
temperature: 0.5,
})
return response.choices[0].message.content
}
// Pipeline 编排
export async function writeArticle(topic: string): Promise<string> {
const titles = await generateTitles(topic)
const bestTitle = await selectBestTitle(titles)
const outline = await generateOutline(bestTitle)
// 每个章节可以并行写
const sections = await Promise.all(
outline.map((section) => writeSection(bestTitle, section))
)
return `# ${bestTitle}\n\n${sections.join('\n\n---\n\n')}`
}这种拆解的好处:
- 每个 Prompt 职责单一——更容易调试,更容易写测试
- 中间结果可控——每一步的输出都能独立检查和记录
- Token 用量可控——不会在一个大 Prompt 里消耗大量上下文
- 可插拔——某一步效果不好,只改那一步的 Prompt,不影响其他步骤
代价是多次 API 调用带来的延迟和成本。但对大多数业务场景来说,可控性比速度更重要。
4. 任务描述的粒度
任务拆到什么粒度合适?没有一个固定的答案,但有几个判断标准。
标准一:一个 Prompt 只做一件事
如果一个 Prompt 里出现了「同时」「并且」「另外」这类连接词,可能意味着它承担了两个任务。
// 两件事混在一个 Prompt 里 ❌
const prompt = `
分析这段代码的性能问题,并且生成对应的单元测试。
`
// 拆成两个独立的 Prompt ✅
const perfPrompt = '分析这段代码的性能问题,列出 top 3 瓶颈。'
const testPrompt = '根据以下代码和性能分析结果,生成单元测试。'标准二:失败时能定位
如果一个 Prompt 效果不好,你能不能定位到是哪部分指令的问题?如果整段 Prompt 是一个整体,改任何一句都可能影响其他部分,调试成本会很高。
标准三:能独立测试
每个子任务都应该能独立准备输入、独立验证输出。如果两个子任务共享同一个 Prompt,它们就没办法独立测试。
5. 动作词的语义系统
一个容易被忽略的细节:同一篇 Prompt 里,描述同类动作的词应该保持一致。
// 混用近义词 ❌ — 模型可能理解为不同的操作
const prompt = `
1. 检查代码中的类型问题
2. 检测性能瓶颈
3. 审查安全漏洞
4. 校验命名规范
`
// 统一动词 ✅ — 模型知道这四个都是「找问题」
const prompt = `
检查以下代码,按类别报告问题:
1. 类型安全:any 滥用、未标注类型、不安全断言
2. 性能问题:不必要的循环、内存泄漏、重复计算
3. 安全风险:SQL 注入、XSS、硬编码密钥
4. 命名违规:不符合项目命名规范的标识符
`第二版里,四个检查项共享同一个动作「检查」,只是类别不同。模型更容易理解这是一个统一的扫描任务。
在工程实践中,建议先钉住一套动作词,后续新增 Prompt 时复用这套词系:
| 动作 | 含义 |
|---|---|
| 检查 | 扫描输入,找出不符合规则的地方 |
| 提取 | 从输入中抽取特定信息 |
| 生成 | 根据输入创建新内容 |
| 转换 | 把输入从一种格式变成另一种格式 |
| 校验 | 判断输入是否符合给定规则 |
| 分类 | 把输入归入预定义的类别 |
6. 一个综合示例
把这一节的内容合在一起,写一个实际的任务描述:
// src/services/prompt/data-extractor.ts
// ❌ 模糊版本
const vaguePrompt = '从用户输入中提取订单信息'
// ✅ 具体版本
const EXTRACT_ORDER_PROMPT = `你是一个订单信息提取助手。
## 任务
从用户的自然语言输入中,提取订单相关字段。
## 需要提取的字段
| 字段名 | 类型 | 说明 |
|---|---|---|
| product_name | string | 商品名称 |
| quantity | number | 数量,默认为 1 |
| unit_price | number | 单价,单位:分 |
| delivery_date | string | 期望配送日期,格式 YYYY-MM-DD |
| address | string | 收货地址 |
## 处理规则
- 数量用中文数字表示时,转成阿拉伯数字(「三个」→ 3)
- 金额用元表示时,转成分(「100 元」→ 10000)
- 日期用相对时间表示时,基于今天计算绝对日期
- 缺少的字段填 null
- 如果输入完全不包含订单信息,返回空对象 {}
## 输出格式
JSON 对象,只包含以上字段。不要输出其他说明文字。`
// Hono 路由
const app = new Hono()
app.post('/extract-order', async (c) => {
const { text } = await c.req.json()
const response = await callModelAPI({
model: 'gpt-4o',
messages: [
{ role: 'system', content: EXTRACT_ORDER_PROMPT },
{ role: 'user', content: text },
],
temperature: 0,
response_format: { type: 'json_object' },
})
return c.json({ order: JSON.parse(response.choices[0].message.content) })
})
export default app这段代码体现了三个层次的到位:
- 动词可执行:「提取」→ 明确了从自然语言到结构化数据的转换
- 对象有边界:五个字段,每个字段的类型和含义都列出来了
- 结果可验证:处理规则明确,可以针对每条规则设计测试用例
7. 常见的任务描述反模式
反模式一:堆砌形容词
请给出一个详细的、全面的、深入的、专业的分析
这些形容词不携带信息。模型不会因为看到「深入的」就给出更深入的分析。把形容词删掉,换成具体的要求:
分析这段代码,覆盖以下三个维度:
1. 时间复杂度
2. 空间复杂度
3. 潜在的边界问题
反模式二:假设模型知道你的约定
按照我们的规范处理这段数据
模型不知道「我们的规范」是什么。要么把规范贴进 Prompt,要么引用一个具体的文档:
按照以下数据处理规范处理:
1. 字段名使用 snake_case
2. 时间字段统一使用 UTC ISO 8601 格式
3. 金额字段单位为分,类型为 integer
反模式三:任务描述和格式约束混在一起
分析这段代码并且输出 JSON 格式的结果要求包含 severity 和 message 两个字段另外还要检查性能问题
信息没有结构,模型很难从中提取出清晰的指令。应该用结构化的方式组织:
## 任务
分析代码问题
## 检查范围
- 类型安全
- 性能问题
## 输出格式
JSON 数组,每个元素包含 severity 和 message 字段
总结
回顾这一节的要点:
- 任务描述的核心:模型读完之后知道该做什么、做到什么程度
- 三个层次:动词可执行、对象有边界、结果可验证
- 复杂任务需要拆解,按步骤拆或按职责拆,后者更可控
- 一个 Prompt 只做一件事,失败时能定位,能独立测试
- 动作词要统一,不要混用近义词
- 反模式:堆砌形容词、假设模型知道你的约定、任务和格式混在一起
下一篇讲角色设定设计——system prompt 怎么写,才能让模型在不同场景下表现出一致的行为特征。