消息格式设计

要点

  • OpenAI Chat Completions API 使用消息数组作为输入
  • 消息有四种角色:system、user、assistant、tool
  • 消息顺序影响对话质量,应该按时间顺序排列
  • 多轮对话需要维护完整的消息历史
  • 上下文窗口有限,需要策略性地管理消息历史
  • 消息格式设计直接影响 AI 的理解和输出质量

1. 消息的基本结构

1.1 消息格式

每条消息包含 rolecontent

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 的理解和输出质量。

这一节涉及到的几个实践:

  1. 消息结构:role + content,四种角色
  2. 多轮对话:维护完整的消息历史
  3. 上下文管理:滑动窗口、Token 计数、摘要压缩、向量检索
  4. 高级格式:多模态、Function Calling、预设消息
  5. 最佳实践:清晰指令、结构化输出、示例引导、分隔符
  6. 消息存储:数据库 Schema 和索引

消息格式设计是 AI 应用开发的核心技能。好的消息设计让 AI 更准确地理解你的意图,产出更高质量的回答。

下一篇看 System、User、Assistant 消息的详细用法和最佳实践。