消息格式设计
要点
- OpenAI Chat Completions API 使用消息数组作为输入
- 消息有四种角色:system、user、assistant、tool
- 消息顺序影响对话质量,应该按时间顺序排列
- 多轮对话需要维护完整的消息历史
- 上下文窗口有限,需要策略性地管理消息历史
- 消息格式设计直接影响 AI 的理解和输出质量
1. 消息的基本结构
1.1 消息格式
每条消息包含 role 和 content:
interface Message {
role: 'system' | 'user' | 'assistant' | 'tool'
content: string | ContentPart[]
name?: string // 可选,用于区分同一角色的不同发送者
}1.2 四种角色
System(系统):
{
role: 'system',
content: '你是一位专业的编程导师,用简单的语言解释概念。'
}- 设定 AI 的行为和约束
- 通常放在消息列表的开头
- 一个对话通常只有一个 system 消息
- 用户通常看不到 system 消息
User(用户):
{
role: 'user',
content: '什么是递归?'
}- 用户的输入
- 可以包含问题、指令、数据
Assistant(助手):
{
role: 'assistant',
content: '递归是一种函数调用自身的编程技术...'
}- AI 的回复
- 在多轮对话中,保留历史 assistant 消息让 AI 记住之前的对话
- 也可以预先写入 assistant 消息来引导 AI 的回复风格
Tool(工具):
{
role: 'tool',
content: '{"temperature": 25, "condition": "sunny"}',
tool_call_id: 'call_123'
}- Function Calling 时,工具的返回结果
- 必须包含
tool_call_id关联到对应的工具调用
2. 多轮对话的消息结构
2.1 基本结构
一个完整的多轮对话消息列表:
const messages = [
// 1. System prompt(设定角色)
{
role: 'system',
content: '你是一位友好的编程导师。'
},
// 2. 历史对话
{ role: 'user', content: '什么是变量?' },
{ role: 'assistant', content: '变量是用来存储数据的容器...' },
{ role: 'user', content: '那什么是常量?' },
{ role: 'assistant', content: '常量是值不能改变的变量...' },
// 3. 当前用户输入
{ role: 'user', content: '如何选择合适的变量名?' },
]2.2 在 Hono 中实现
import { Hono } from 'hono'
import { db } from './lib/db'
import { conversations, messages } from './schema'
import { eq } from 'drizzle-orm'
const app = new Hono()
app.post('/api/chat/:conversationId', async (c) => {
const conversationId = c.req.param('conversationId')
const { content } = await c.req.json()
// 1. 获取对话历史
const conversation = await db.query.conversations.findFirst({
where: eq(conversations.id, conversationId),
with: {
messages: {
orderBy: (messages, { asc }) => [asc(messages.createdAt)],
},
},
})
// 2. 构建消息列表
const messageHistory = [
{ role: 'system', content: '你是一位友好的编程导师。' },
...conversation.messages.map(m => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
{ role: 'user', content },
]
// 3. 调用 LLM
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4',
messages: messageHistory,
}),
})
const data = await response.json()
const reply = data.choices[0].message.content
// 4. 保存用户消息和 AI 回复
await db.insert(messages).values([
{ conversationId, role: 'user', content },
{ conversationId, role: 'assistant', content: reply },
])
return c.json({ reply })
})3. 上下文管理策略
3.1 问题
LLM 有上下文窗口限制:
- GPT-4: 8K / 32K / 128K tokens
- Claude: 100K tokens
- Gemini: 1M tokens
多轮对话可能超出限制。
3.2 滑动窗口
只保留最近 N 轮对话:
function slidingWindow(messages: Message[], maxTurns = 10): Message[] {
const system = messages.filter(m => m.role === 'system')
const conversation = messages.filter(m => m.role !== 'system')
// 只保留最近 N 轮(每轮 2 条消息:user + assistant)
const recent = conversation.slice(-maxTurns * 2)
return [...system, ...recent]
}3.3 Token 计数
精确控制上下文长度:
import { encoding_for_model } from 'tiktoken'
const encoder = encoding_for_model('gpt-4')
function countTokens(text: string): number {
return encoder.encode(text).length
}
function truncateToTokenLimit(
messages: Message[],
maxTokens: number
): Message[] {
const system = messages.filter(m => m.role === 'system')
const conversation = messages.filter(m => m.role !== 'system')
let totalTokens = system.reduce(
(sum, m) => sum + countTokens(m.content as string),
0
)
const result = [...system]
// 从最新的消息开始添加
for (let i = conversation.length - 1; i >= 0; i--) {
const msg = conversation[i]
const tokens = countTokens(msg.content as string)
if (totalTokens + tokens > maxTokens) {
break
}
result.splice(1, 0, msg) // 插入到 system 之后
totalTokens += tokens
}
return result
}3.4 摘要压缩
对旧对话进行摘要:
async function compressHistory(messages: Message[]): Promise<Message[]> {
if (messages.length <= 20) {
return messages // 消息不多,不需要压缩
}
const system = messages.filter(m => m.role === 'system')
const recent = messages.slice(-10) // 保留最近 5 轮
const old = messages.filter(m => m.role !== 'system').slice(0, -10)
// 对旧消息进行摘要
const summaryPrompt = `
将以下对话总结为简短的摘要:
${old.map(m => `${m.role}: ${m.content}`).join('\n')}
请用 2-3 句话总结关键点。
`
const response = await callLLM(summaryPrompt)
const summary = response.choices[0].message.content
return [
...system,
{ role: 'system', content: `之前的对话摘要:${summary}` },
...recent,
]
}3.5 向量检索
从历史对话中检索相关内容:
async function retrieveRelevantMessages(
query: string,
allMessages: Message[]
): Promise<Message[]> {
// 将查询向量化
const queryEmbedding = await getEmbedding(query)
// 将历史消息向量化(可以预先计算并存储)
const messageEmbeddings = await Promise.all(
allMessages.map(async (m) => ({
message: m,
embedding: await getEmbedding(m.content as string),
}))
)
// 计算相似度,选择最相关的
const relevant = messageEmbeddings
.map((m) => ({
message: m.message,
similarity: cosineSimilarity(queryEmbedding, m.embedding),
}))
.sort((a, b) => b.similarity - a.similarity)
.slice(0, 10) // 取 top 10
return relevant.map((r) => r.message)
}4. 高级消息格式
4.1 多模态消息
GPT-4V 支持图片和文本:
const messages = [
{
role: 'user',
content: [
{
type: 'text',
text: '这张图片里有什么?',
},
{
type: 'image_url',
image_url: {
url: 'https://example.com/image.jpg',
},
},
],
},
]4.2 Function Calling 消息
const messages = [
{ role: 'user', content: '北京今天天气怎么样?' },
// AI 决定调用工具
{
role: 'assistant',
content: null,
tool_calls: [
{
id: 'call_123',
type: 'function',
function: {
name: 'get_weather',
arguments: '{"city": "北京"}',
},
},
],
},
// 工具返回结果
{
role: 'tool',
tool_call_id: 'call_123',
content: '{"temperature": 25, "condition": "晴天"}',
},
// AI 基于工具结果回答
{
role: 'assistant',
content: '北京今天晴天,温度 25°C。',
},
]4.3 预设 Assistant 消息
可以预设 assistant 消息来引导回复风格:
const messages = [
{ role: 'system', content: '你是一位友好的编程导师。' },
{ role: 'user', content: '什么是递归?' },
{
role: 'assistant',
content: '递归就像俄罗斯套娃...', // 预设回复风格
},
{ role: 'user', content: '能给个代码例子吗?' },
]5. 消息设计最佳实践
5.1 清晰的指令
// ❌ 模糊
{ role: 'user', content: '帮我看看这个代码' }
// ✅ 清晰
{
role: 'user',
content: `请审查以下 TypeScript 代码,指出:
1. 潜在的 bug
2. 性能问题
3. 代码风格问题
代码:
${code}`
}5.2 结构化的输出要求
{
role: 'user',
content: `分析以下数据,以 JSON 格式返回:
数据:${data}
输出格式:
{
"summary": "摘要",
"insights": ["洞察1", "洞察2"],
"recommendations": ["建议1", "建议2"]
}`
}5.3 示例引导
{
role: 'user',
content: `将以下文本分类为正面、负面或中性。
示例:
文本:太棒了!
分类:正面
文本:真糟糕
分类:负面
现在分类:
文本:${userInput}`
}5.4 分隔符
使用分隔符清晰区分不同部分:
{
role: 'user',
content: `根据以下文档回答问题。
===文档开始===
${document}
===文档结束===
问题:${question}
请只根据文档内容回答,如果文档中没有答案,请说"文档中未提及"。`
}6. 消息存储
6.1 数据库 Schema
export const messages = pgTable('messages', {
id: uuid('id').primaryKey().defaultRandom(),
conversationId: uuid('conversation_id').notNull(),
role: text('role').notNull(), // system, user, assistant, tool
content: text('content').notNull(),
metadata: jsonb('metadata'), // 额外信息,如 tool_calls
tokenCount: integer('token_count'),
createdAt: timestamp('created_at').defaultNow().notNull(),
})6.2 消息索引
为快速查询添加索引:
export const messages = pgTable('messages', {
// ...
}, (table) => ({
conversationIdx: index('messages_conversation_idx').on(table.conversationId),
createdAtIdx: index('messages_created_at_idx').on(table.createdAt),
}))总结
消息格式是与 LLM 沟通的基础。合理的消息设计能显著提升 AI 的理解和输出质量。
这一节涉及到的几个实践:
- 消息结构:role + content,四种角色
- 多轮对话:维护完整的消息历史
- 上下文管理:滑动窗口、Token 计数、摘要压缩、向量检索
- 高级格式:多模态、Function Calling、预设消息
- 最佳实践:清晰指令、结构化输出、示例引导、分隔符
- 消息存储:数据库 Schema 和索引
消息格式设计是 AI 应用开发的核心技能。好的消息设计让 AI 更准确地理解你的意图,产出更高质量的回答。
下一篇看 System、User、Assistant 消息的详细用法和最佳实践。