作者:一位Android开发工程师 | 2026年5月31日
系列:第3天已掌握system prompt,本篇拆解多轮对话的底层原理


前言

前三天我学会了控制LLM的输出行为(temperature)、人格设定(system prompt)、输出格式(JSON/表格)。但这些都是"一问一答"——每次对话互不关联。

今天要解决的问题:怎么让LLM记住上一句说了什么?


核心认知:LLM本身没有记忆

如果你以为LLM自带记忆功能,那就想多了。LLM的"记忆"是一个假象——你只是把之前所有对话塞进 messages 数组,每次都发回去

类比Android开发:就像 RecyclerView 加载更多数据时,不是在新的Adapter里加载,而是往同一个List里追加。messages数组同理。


实验一:多轮面试对话

把面试官的人设写进system prompt,然后模拟三轮面试对话:

messages = [
    {"role": "system", "content": "你是Android面试官,考察Kotlin基础。"},
]

def chat(user_input):
    messages.append({"role": "user", "content": user_input})
    response = client.chat.completions.create(
        model="turing/gpt-4o-mini",
        messages=messages,  # ← 关键:每次都传整个数组
    )
    reply = response.choices[0].message.content
    messages.append({"role": "assistant", "content": reply})
    return reply

注意这行 messages=messages——每次请求都把全部历史发回去,而不是只发当前问题。

三轮对话结果

第1轮
你:val和var有什么区别?
面试官:val是不可变的(类似Java的final),var是可变的...

第2轮  
你:那刚才说的val变量,值真的永远不能变吗?
面试官:引用不可变,但如果引用的是可变对象(如MutableList),
       对象的内容可以改变...  ← 引用了第一轮的"val",给出了更深入的解释

第3轮
你:再回到第一个问题,给我总结一下核心区别
面试官:val=不可变引用,var=可变引用...  ← 回到了第一轮的内容

第三轮能准确回到第一轮。不是什么魔法,是 messages 数组里存着完整对话历史。

代价

三轮对话下来,messages 数组涨到了7条:

[0] system: 你是Android面试官...
[1] user: val和var有什么区别?
[2] assistant: val不可变,var可变...
[3] user: 那刚才说的val变量...
[4] assistant: 引用不可变,对象内容可以变...
[5] user: 再回到第一个问题...
[6] assistant: val=不可变引用...

每条消息都占token。对话越长,每轮cost越高。


实验二:决定性实验——带历史vs不带历史

这个实验最能说明问题。先问一个基础问题,再追问一个模糊问题:

第一问:Kotlin的扩展函数是什么?
LLM:在不修改类源码的情况下添加新功能的特性...

追问:给我一个具体的例子  ← 只说"例子",没说什么的例子

方式A:不带历史

messages = [{"role": "user", "content": "给我一个具体的例子"}]
# 只发了追问本身

结果:

LLM:请问您想要什么类型的具体例子呢?生活中的、科学实验的、
     还是其他方面的?请提供更多信息...

😵 LLM完全不知道你要什么例子——它没看到上一句"扩展函数"。

方式B:带历史

messages = [
    {"role": "user", "content": "Kotlin的扩展函数是什么?"},
    {"role": "assistant", "content": "扩展函数是..."},  # 上一轮的完整回答
    {"role": "user", "content": "给我一个具体的例子"},
]

结果:

LLM:为 List 添加一个扩展函数 sumBy...  // Kotlin扩展函数代码示例

✅ 它知道你要的是"扩展函数"的例子——因为历史里有上下文。


LLM"记忆"的真相

第1轮:messages 有 2 条 → token少,响应快
第3轮:messages 有 7 条 → token中等
第10轮:messages 有 21 条 → 开始慢
第20轮:messages 有 41 条 → 又慢又贵

messages每轮增加user+assistant两条消息,线性增长。第20轮时相当于把前19轮的完整对话全部重新发给LLM——绝大部分token都浪费在重复传输历史消息上

解决方案:记忆系统(后面会学)

成熟的Agent不会傻傻地把全部历史都发回去。有两种策略:

  1. 上下文窗口裁剪:只保留最近N轮对话,旧的丢掉
  2. 长期记忆:把旧对话压缩成摘要存入向量数据库,需要时检索

这就像Android的 LruCache——内存有限,只保留最近最常用的,旧的存入磁盘。


今天的一句话总结

LLM没有记忆——它看起来记住了上下文,只是因为你把全部历史对话塞进messages数组每次发回去。对话越长越贵,需要记忆系统来优化。


阶段性总结

四天学完,一次完整LLM API调用的全部要素已经齐了:

response = client.chat.completions.create(
    model="...",           # 第1天:模型选择
    messages=[             # 第1天:消息数组
        {...},             # 第3天:system人设/格式/约束
        {...}, {...},      # 第4天:历史对话(多轮记忆)
    ],
    temperature=0.3,       # 第2天:创造力控制
    max_tokens=250,        # 第2天:长度限制
)

六个参数覆盖了单次API调用的全部要素。后面Agent、RAG、工具调用,都在这基础上扩展。


下一篇预告

第5天:Few-shot提示——给LLM看几个例子,让它模仿格式输出。Prompt Engineering的最后一个核心技巧。


本系列记录一位Android开发者转行AI Agent的完整学习过程,欢迎关注交流。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐