3.3 对话能力集成:多轮对话管理与上下文持久化实现

1 阅读4分钟

3.3 对话能力集成:多轮对话管理与上下文持久化实现

一、多轮对话核心

多轮对话需要维护会话上下文,将历史消息在每次请求时一并发送给模型。模型依赖完整上下文才能理解"你"、"它"等指代,以及延续之前的话题。本节介绍上下文管理、长度控制与持久化方案,并提供可直接运行的完整代码。


二、上下文管理

2.1 内存管理

单机、单进程场景下,可用字典按session_id存储每个会话的messages

# 会话级messages(内存存储)
session_messages = {}

def get_or_create_messages(session_id: str, system_prompt: str = "你是有帮助的助手"):
    if session_id not in session_messages:
        session_messages[session_id] = [
            {"role": "system", "content": system_prompt}
        ]
    return session_messages[session_id]

def append_and_chat(session_id: str, user_input: str, client) -> str:
    messages = get_or_create_messages(session_id)
    messages.append({"role": "user", "content": user_input})
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        temperature=0.7,
        max_tokens=500
    )
    reply = response.choices[0].message.content
    messages.append({"role": "assistant", "content": reply})
    return reply

注意:进程重启后内存清空,生产环境需持久化。

2.2 长度控制策略

当对话轮次增多,messages可能超出模型的上下文窗口(如gpt-3.5-turbo的16K)。常用策略:

策略实现方式优点缺点
滑动窗口只保留最近N轮(如10轮)简单、成本可控早期信息丢失
摘要压缩将早期对话用LLM摘要,替换为一条摘要消息保留关键信息摘要可能失真、额外API调用
Token计数截断用tiktoken估算,超限时从最早的消息开始删除精确控制长度实现稍复杂
关键信息提取只保留对后续有用的片段(如用户偏好、任务目标)信息密度高提取逻辑复杂

2.3 滑动窗口实现示例

def trim_to_last_n_turns(messages: list, n: int = 10) -> list:
    """保留system + 最近n轮对话"""
    if len(messages) <= 1 + n * 2:  # system + n轮(user+assistant)
        return messages
    result = [messages[0]]  # 保留system
    result.extend(messages[-(n * 2):])  # 最近n轮
    return result

2.4 基于Token的截断

import tiktoken

def trim_by_tokens(messages: list, max_tokens: int = 4000, model: str = "gpt-3.5-turbo") -> list:
    """从最早的消息开始删除,直至总Token不超过max_tokens"""
    enc = tiktoken.encoding_for_model(model)
    total = sum(len(enc.encode(m.get("content", ""))) for m in messages)
    if total <= max_tokens:
        return messages
    result = [messages[0]]  # 保留system
    for m in reversed(messages[1:]):
        total -= len(enc.encode(m.get("content", "")))
        result.insert(1, m)
        if total <= max_tokens:
            break
    return result

三、持久化方案

3.1 Redis存储

适合分布式、高并发场景。将messages序列化为JSON存储,设置过期时间避免内存占满。

import json
import redis

r = redis.Redis(host='localhost', port=6379, db=0)

def save_messages(session_id: str, messages: list, ttl: int = 86400):
    """保存会话,默认24小时过期"""
    r.setex(f"chat:{session_id}", ttl, json.dumps(messages, ensure_ascii=False))

def load_messages(session_id: str) -> list:
    data = r.get(f"chat:{session_id}")
    return json.loads(data) if data else None

3.2 数据库存储

适合需长期保存、审计、分析的场景。可设计表结构:

字段类型说明
session_idvarchar会话ID
rolevarcharsystem/user/assistant
contenttext消息内容
created_atdatetime创建时间

查询时按session_id、created_at排序,组装为messages列表。

3.3 方案选型

方案适用场景优点缺点
内存开发、单机简单、无依赖重启丢失、无法扩展
Redis分布式、高并发快速、支持过期需维护Redis
数据库长期保存、审计持久、可分析读写较慢、需设计表结构

四、完整示例:带持久化的多轮对话服务

"""
多轮对话服务(支持Redis持久化与长度控制)
运行: pip install openai python-dotenv redis tiktoken
"""

import json
import os
from dotenv import load_dotenv
from openai import OpenAI
import tiktoken

load_dotenv()
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 若未安装redis,可改用内存字典
try:
    import redis
    r = redis.Redis(host=os.getenv("REDIS_HOST", "localhost"), port=6379, db=0)
    USE_REDIS = True
except:
    USE_REDIS = False
    memory_store = {}

def get_messages(session_id: str) -> list:
    if USE_REDIS:
        data = r.get(f"chat:{session_id}")
        return json.loads(data) if data else [{"role": "system", "content": "你是有帮助的助手"}]
    return memory_store.get(session_id, [{"role": "system", "content": "你是有帮助的助手"}])

def save_messages(session_id: str, messages: list):
    if USE_REDIS:
        r.setex(f"chat:{session_id}", 86400, json.dumps(messages, ensure_ascii=False))
    else:
        memory_store[session_id] = messages

def trim_messages(messages: list, max_tokens: int = 3500) -> list:
    enc = tiktoken.encoding_for_model("gpt-3.5-turbo")
    total = sum(len(enc.encode(m.get("content", ""))) for m in messages)
    if total <= max_tokens:
        return messages
    result = [messages[0]]
    for m in reversed(messages[1:]):
        total -= len(enc.encode(m.get("content", "")))
        result.insert(1, m)
        if total <= max_tokens:
            break
    return result

def chat(session_id: str, user_input: str) -> str:
    messages = get_messages(session_id)
    messages.append({"role": "user", "content": user_input})
    messages = trim_messages(messages)
    response = client.chat.completions.create(
        model="gpt-3.5-turbo",
        messages=messages,
        temperature=0.7,
        max_tokens=500
    )
    reply = response.choices[0].message.content
    messages.append({"role": "assistant", "content": reply})
    save_messages(session_id, messages)
    return reply

五、小结

多轮对话的核心是上下文管理与长度控制。根据业务需求选择内存、Redis或数据库持久化,并做好Token与轮次限制,避免超长上下文导致的错误与高成本。下一节将介绍GPT-4V图像理解与多模态集成。


下一节预告:3.4 文本生成与多模态能力集成:GPT-4V图像理解实战