当前位置: 首页 > news >正文

【Agent开发第三期】短期记忆history,让模型“记住“上一句

文章目录

    • 一、前言
    • 二、概念对齐:为什么"回灌"能让模型记住
      • 2.1 回顾:模型为什么失忆
      • 2.2 回灌历史:从"一句话"到"完整历史"
      • 2.3 一张表看清三期演进
      • 2.4 本期的"组装公式"
    • 三、动手做:从金鱼记忆到短期记忆
      • 3.1 维护一个 messages 列表
      • 3.2 上下文长度限制:历史不能无限长
      • 3.3 reset 和 history 命令
      • 3.4 自动演示:对比第 02 期的"金鱼记忆"
    • 四、跑起来:短期记忆的实际体验
      • 4.1 为什么回灌历史能让模型"记住"?
      • 4.2 token 增长可视化
      • 4.3 调用流程(与前两期一致)
    • 五、执行脚本
    • 六、总结

一、前言

第 02 期你跑通了循环对话,但模型有"金鱼记忆"——你上一句说"我叫张三",下一句问"我姓什么",它答不上来。明明刚说过,转头就忘。

根本原因:每次调用messages里只传当前这一句,模型看不到历史。这不是模型笨,是你没把历史喂给它。

这一期我们就动手维护一份messages列表,每次调用把完整对话历史回灌进去,让模型真正"记住"上一句。同时引入上下文长度限制——历史不能无限长,这是个新问题。看完你就能:

  • 用 messages 列表维护完整对话历史
  • 理解"回灌历史"为什么能让模型"记住"
  • 用 token 计数直观看到上下文在增长
  • 加一个最简单的上下文长度限制,防止历史无限膨胀

本文是 Agent 教学系列第 03 期的实战笔记,干货为主,偶尔自嘲,各位看官将就着看。

二、概念对齐:为什么"回灌"能让模型记住

2.1 回顾:模型为什么失忆

上一期讲过,模型 API 是无状态的——每次请求都被当成全新对话,服务器不保存任何上下文。所以"连续对话"的幻觉,是客户端伪造出来的:你自己维护一份历史,每次请求把整段历史一起发过去。

但第 02 期我们故意没这么做——messages里永远只有当前这一句,专门让你踩一脚"金鱼记忆"。这一期补上。

2.2 回灌历史:从"一句话"到"完整历史"

核心改动就一处:把chat()的入参从"一句话"变成"完整 messages 列表"。

defchat(messages:list[dict])->tuple[str,int,int]:response=client.chat.completions.create(model=MODEL,messages=messages,# ← 完整历史,不再只传当前一句max_tokens=1000,)...

每次对话的流程:

  1. 用户输入 →{"role": "user", "content": 输入}加入 messages
  2. 调用 API,把完整 messages 发过去
  3. 模型回复 →{"role": "assistant", "content": 回复}也加入 messages
  4. 下一次调用,messages 里已经带着之前的全部问答

这样模型每次都能"看到"完整上下文,"金鱼记忆"问题解决。

2.3 一张表看清三期演进

第 01 期第 02 期第 03 期
调用方式单次一问一答while 循环while 循环 + 历史回灌
messages 数组只传当前一句只传当前一句完整对话历史
记忆能力无(故意暴露痛点)有(短临记忆)
token 观察每次 prompt 都很小prompt 随历史增长

2.4 本期的"组装公式"

短期记忆 = messages[] 回灌 + 上下文长度限制

把历史塞进去,模型就"记住"了;但历史不能无限长,得加个上限。

三、动手做:从金鱼记忆到短期记忆

3.1 维护一个 messages 列表

用一个 list 在内存里维护对话历史,角色严格按user/assistant交替:

messages:list[dict]=[]# 用户输入messages.append({"role":"user","content":user_input})# 调用 API(传完整历史)answer,p,c=chat(messages)# 模型回复也加回历史messages.append({"role":"assistant","content":answer})

这样下一次调用时,messages 里已经带着之前的全部问答,模型能"看到"上下文。

3.2 上下文长度限制:历史不能无限长

历史一直累积,prompt_tokens会越滚越大,最终触发模型的上下文长度上限(DeepSeek 通常 64K token)。

本期用最简单的截断策略:

MAX_ROUNDS=10# 1 轮 = 1 user + 1 assistantdeftrim_history(messages:list[dict])->list[dict]:rounds=sum(1forminmessagesifm["role"]=="user")ifrounds<=MAX_ROUNDS:returnmessages excess_rounds=rounds-MAX_ROUNDS cut=excess_rounds*2# 每轮 2 条returnmessages[cut:]

超过 10 轮时,删掉最早的一轮,保留最近 10 轮。简单粗暴,但有边界。

第 07 期会升级为 compact 压缩策略(把旧对话摘要成一段,而非直接删),本期先解决"有没有"的问题。

3.3 reset 和 history 命令

  • reset:清空 messages 列表,重新开始(第 02 期埋的伏笔,这期兑现)
  • history:打印当前 messages 列表,直观看到[user, assistant, user, assistant, ...]的结构

这两个命令不是花架子,是帮你调试和理解"历史是怎么组织的"。

3.4 自动演示:对比第 02 期的"金鱼记忆"

程序跑起来会先自动问两个问题(和第 02 期一模一样):

  1. “我叫张三,是一名 Python 后端工程师” —— 告诉模型信息
  2. “我姓什么?我是做什么工作的?” —— 看它能否接住

这次模型应该能答上来——因为 messages 里带着上一轮的问答。同时你会看到第 2 轮的prompt_tokens比第 2 期大,因为历史一起发了过去。

四、跑起来:短期记忆的实际体验

4.1 为什么回灌历史能让模型"记住"?

模型本身是无状态的,但每次调用都会读到你传入的完整messages。只要历史在 messages 里,模型就能"看到"之前说过什么。

第 1 轮: messages = [user: "我叫张三"] → 模型回复 "你好,张三!" messages = [user: "我叫张三", assistant: "你好,张三!"] 第 2 轮: messages = [user: "我叫张三", assistant: "你好,张三!", user: "我姓什么?"] → 模型能看到前两条,答 "你姓张"

4.2 token 增长可视化

对比第 02 期,本期的prompt_tokens会明显更大,因为历史一起发了过去:

轮次第 02 期 prompt_tokens第 03 期 prompt_tokens
第 1 轮~10(一句话)~10(一句话)
第 2 轮~10(还是一句话)~30(含第 1 轮的问答)
第 5 轮~10~150(含前 4 轮)

这就是上下文长度问题的由来——历史越多,token 越贵,也越容易触顶。

4.3 调用流程(与前两期一致)

你的代码 → openai SDK → HTTPS 请求(带完整 messages) → DeepSeek → 推理 → 返回 JSON + usage

相比第 02 期,唯一的差异是:请求体里的messages从一句话变成了完整历史。

五、执行脚本

#!/usr/bin/env python3"""step03_history.py — 第 03 期:短期记忆 history 本期目标: 1. 维护一个 messages 列表,每次调用把完整对话历史回灌进去 2. 让模型真正"记住"上一句,解决第 02 期的"金鱼记忆" 3. 引入上下文长度限制,历史不能无限长——超限时做最简单的截断 累积式:step03 = step02 + messages[] 回灌 + 上下文长度限制 (尚未做记忆压缩/长期记忆,后续第 07 期解决) 运行: python code/step03_history.py """importosfromdotenvimportload_dotenvfromopenaiimportOpenAIfromprompt_toolkitimportpromptfromprompt_toolkit.historyimportInMemoryHistory load_dotenv()# ============ 1. 初始化客户端(与第 01/02 期一致)============client=OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"],base_url=os.environ.get("DEEPSEEK_BASE_URL","https://api.deepseek.com"),)MODEL=os.environ.get("DEEPSEEK_MODEL","deepseek-chat")# 上下文长度上限(轮数,1 轮 = 1 user + 1 assistant)MAX_ROUNDS=10defchat(messages:list[dict])->tuple[str,int,int]:"""带历史回灌的对话:传入完整 messages 列表,返回 (回答, prompt_tokens, completion_tokens)。"""response=client.chat.completions.create(model=MODEL,messages=messages,max_tokens=1000,)answer=response.choices[0].message.content usage=response.usagereturnanswer,usage.prompt_tokens,usage.completion_tokensdeftrim_history(messages:list[dict])->list[dict]:"""上下文长度限制:超过 MAX_ROUNDS 轮时,从最早的一轮开始截断。"""rounds=sum(1forminmessagesifm["role"]=="user")ifrounds<=MAX_ROUNDS:returnmessages excess_rounds=rounds-MAX_ROUNDS cut=excess_rounds*2returnmessages[cut:]defmain():print("="*60)print("第 03 期:短期记忆 history —— 让模型记住上一句")print("="*60)print(f"当前模型:{MODEL}")print(f"API 地址:{client.base_url}")print(f"上下文上限:{MAX_ROUNDS}轮(超出自动截断最早)")print("="*60)# ============ 2. 自动演示:对比第 02 期的"金鱼记忆" ============demo_questions=["我叫张三,是一名 Python 后端工程师","我姓什么?我是做什么工作的?",]print("\n[自动演示] 同样两个问题,这次模型能否记住上一句:\n")messages:list[dict]=[]fori,qinenumerate(demo_questions,1):print(f"[问题{i}]{q}")messages.append({"role":"user","content":q})try:answer,p,c=chat(messages)print(f"[回答]{answer}")print(f"[token] prompt={p}completion={c}total={p+c}")print(f"[历史] 当前 messages 共{len(messages)}条")messages.append({"role":"assistant","content":answer})exceptExceptionase:print(f"[出错]{e}")returnprint("-"*60)print("\n💡 对比第 02 期:这次模型答上来了——""因为 messages 里带着上一轮的问答,模型"看到"了完整上下文。\n"" 但注意 prompt_tokens 比第 02 期大,因为历史一起发了过去。\n")# ============ 3. 交互式对话(带历史回灌)============print("="*60)print("现在进入自由对话(有记忆,但超过 10 轮自动截断最早)")print("输入 quit 退出 / 输入 reset 清空历史 / 输入 history 查看历史")print("←/→ 移动光标,↑/↓ 翻历史,Ctrl-C 作废当前行重输")print("="*60)total_prompt=0total_completion=0cli_history=InMemoryHistory()whileTrue:try:user_input=prompt("\n你: ",history=cli_history).strip()except(EOFError,KeyboardInterrupt):print(" (本行作废,重新输入)")continueifuser_input.lower()in("quit","exit","q"):print(f"\n[本次会话 token 汇总] prompt={total_prompt}"f"completion={total_completion}total={total_prompt+total_completion}")print(f"[历史] 退出时共{len(messages)}条消息")print("再见!")breakifuser_input.lower()=="reset":messages.clear()total_prompt=0total_completion=0print("[提示] 历史已清空,重新开始对话。")continueifuser_input.lower()=="history":print(f"[历史] 共{len(messages)}条消息:")foridx,minenumerate(messages):role=m["role"]content=m["content"][:50]+("..."iflen(m["content"])>50else"")print(f"{idx:2d}. [{role:9s}]{content}")continueifnotuser_input:continuemessages.append({"role":"user","content":user_input})messages=trim_history(messages)try:answer,p,c=chat(messages)total_prompt+=p total_completion+=c messages.append({"role":"assistant","content":answer})print(f"Alex:{answer}")print(f"[token] 本次 prompt={p}completion={c}"f"累计 total={total_prompt+total_completion}")print(f"[历史] 当前{len(messages)}条消息"f"({sum(1forminmessagesifm['role']=='user')}轮)")exceptExceptionase:messages.pop()print(f"[出错]{e}")if__name__=="__main__":main()

六、总结

一句话回顾:模型本身无记忆,记忆是你用 messages 列表"喂"给它的。

三个关键动作记牢:

  1. 维护 messages 列表→ 每轮把 user + assistant 都加进去
  2. 回灌历史→ 调用时传完整 messages,模型就能"看到"上下文
  3. 上下文限制→ 历史不能无限长,超限时截断最早(下期升级为压缩)

一行代码记住本期:

messages.append({"role":"assistant","content":answer})# ← 回复也加回历史

把模型回复也加入 messages,下一轮调用时它就"记住"了。

适用场景:这篇适合刚跑通第 02 期、想让模型真正"记住"对话的人。

下一期预告:第 04 期——System Prompt 人设。我们用templates/SOUL.md给 Agent 设定"技术助理 Alex"的角色,对比有无 system prompt 时的行为差异。同时 system prompt 会成为 messages 列表的第一条,进一步影响上下文。


感谢各位看官的一路陪伴,大家都再接再厉!

http://www.jsqmd.com/news/1310972/

相关文章:

  • MoE架构破局:Wan2.2如何将视频生成成本压至$0.21?
  • 零基础玩转bWAPP靶场(三十一):XML/XPath 注入(搜索)
  • 上下文省了 857 倍,路由却瞎掉 72%:50 个 Agent Skill 渐进式加载的实测复盘
  • 2026 年当下,合川有实力的卷扬启闭机生产厂家哪家好,用了30年的水利老物件,如今竟靠它省下百万运维费? -岳江水工机械 - 行业推荐官[官方】--
  • 以前我总是看不起单元测试,不过现在我知道我错了
  • AI如何变革理论物理研究:从Scaling Law到人机协同新范式
  • MinMaxScaler归一化:从原理到实战,掌握数据预处理的公平法则
  • 1.33英寸E-Ink屏幕驱动全解析:从SPI通信到低功耗信息屏实战
  • MSI-X中断机制详解:从原理到Linux内核实践与性能调优
  • 2026 年现阶段郑州到拉萨物流专线公司怎么联系,去拉萨寄大件,居然能省这么多?藏区老货代藏了十年的秘密被扒出来了-创青轿车托运物流专线 - 行业推荐官[官方】--
  • Python SQLite ‘no such table‘错误全解析:从连接事务到ORM框架的深度排查指南
  • Python实现指纹图像增强:Gabor滤波与方向场估计实战
  • AI驱动的日志异常检测:3步实现99.99%准确率,告别人工巡检时代
  • 游戏开发架构优化:从ECS到事件驱动,解决音游性能与维护难题
  • 从乱码到中文:解码\x字符串的编码原理与实战解决方案
  • 2026 年更新:西安到锦州商务车托运公司哪个好,赶海返程的人别乱选,这玩意儿能让你的商务车托运全程省心省力还不踩坑? - 企业官方推荐【认证】
  • 195、NPU的编译器开发:文档编写与API设计
  • OpenHarmony赋能6G智能知识平台
  • STM32G431 FOC电机控制:从代码搬运到逻辑掌控的调试实战
  • 从零构建沉浸式解谜游戏:状态机、场景管理与数据驱动架构实践
  • HarmonyOS NEXT 实战:基于 Want 与 fileUri 的文件分享功能实现
  • 文件上传漏洞攻防全解析:从一句话木马构造到立体防御体系构建
  • 树莓派、Arduino与STM32驱动3.52英寸电子纸屏幕全攻略
  • 2026精选陕西展馆设计装修施工总包机构——赛野展示展馆模型 - 装修教育财税推荐2026
  • 研学旅行实训室技术架构:VR全景底座+同伴互动AI七维测评+3DGS采集+数字人
  • 深入解析PID双环控制:从原理到STM32嵌入式实现
  • KKCE 在线 Ping 检测工具运维实战指南
  • mPBPK建模:破解药物入脑难题,优化脑肿瘤精准给药方案
  • 数字IC面试必考:同步FIFO设计原理与工业级实现详解
  • 区块链电力交易系统开发实战:从软件工程到智能合约的完整项目复盘