前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8370
  • Claude红队技能库:SQL注入到Shellcode全覆盖
  • 多仓库 AI 代码审查:本质是上下文问题
  • Grok 4.8 训练完成:2.5 万亿参数
  • Ollama Responses API 会话记忆失效 Bug
  • Bengio 深度解析 AI Agent 作弊机制
  • Perplexity 用 GPT-6 Astra 接管端到端系统
  • Cache hit价格差167倍:DeepSeek重塑模型成本认知
  • 自建测试框架的 6 项校验清单
  • AI Agent 生产稳定性:重启模式实战分析
  • .NET MCP 工具类运行时崩溃的两行修复
  • AI 编程 Agent 失控:一次 29 轮循环的事后分析
  • 主流大模型System Prompt泄露合集
  • MCP Python SDK 2.0升级避坑指南
  • 普林斯顿提出 RLT:跨 Token 携带解码器状态的循环 Transformer
  • Cursor/Claude 项目审计:四大常见安全陷阱
  • AI Agent 攻入软件供应链:RubyGems 2000 包投送事件分析
  • 微软教程合集:Copilot 换模型、DeepSeek on Azure、Rust、.NET 10 RC、AI Agen
  • 你的网站能被 ChatGPT 索引吗?
  • Claude Code 三种扩展机制对比与避坑指南
  • Iris-mini/pro:开源权重搜索 Agent 登顶同类榜首
  • GPT-6 Astra 无人机控制与商业运营测试:首次全面超越人类基准
  • 企业级 Claude API 治理工具包:PII 扫描 + 审计 + 成本控制
  • 后量子 TLS 是平台迁移,而非密码学项目
  • 两年大学研究:禁止课堂 AI 反而让学生表现更差
  • 编程 Agent 的 Plan-First 工作流:避免「改完才发现没做完」
  • AWS 开源 Pizza Bot:面向后台 AI Agent 的持久化任务收件箱
  • Worktrunk:让AI并行开发更丝滑的Git Worktree CLI
  • KIRA:本地优先的 AI Agent,需用工具结果证明任务完成
  • Context 工程:四大机制解决长程任务上下文溢出与目标丢失
  • MCP 服务器合规性测试:31 个流行服务仅 3% 通过契约验证
  • AI Agent记忆三种实现方式:滑动窗口、摘要与向量存储
  • 让Claude跨会话记住你的偏好:Projects与CLAUDE.md实践
  • OpenAI 用 AI 写代码再用 AI 审查:Codex 安全门禁实践
  • 生产环境 LLM 推理性能与可靠性优化实战
  • 已加载 34 / 8370
8.0
热点
AI SCORE
工具产品2026-09-14 10:10

Ollama Responses API 会话记忆失效 Bug

dev.to · AI#Ollama#Bug#API
Editor brief · 编辑速览

传递 previous_response_id 字段时被 JSON decoder 静默丢弃,API 返回 200 但模型完全丢失上下文,这是一个隐蔽但严重的 bug。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

TL;DR:将 previous_response_id 发送给 Ollama 的 /v1/responses,请求会被接受,返回 HTTP 200,状态为 "status": "completed"、"error": null,但模型根本没有看到上一轮对话。携带 previous_response_id 的请求与不携带历史记录的请求消耗的输入 token 数量相同——在这个测试里都是 41 个——并产生同样离谱的回答。改用完整历史记录则消耗 68 个 token 且能正常工作。原因藏在源码里:请求结构体没有 previous_response_id 字段,所以 JSON 解码器直接把它丢弃了;响应结构体虽然认识这个字段,但将其设为 nil 并注释 // Not supported。由此衍生出的 bug 报告——托管模型在工具调用后返回空完成——本质上就是一个没有任何上下文的裸 function_call_output,后端面对这种输入给出了一个"没什么好说的"回复。

环境:Ollama 0.34.0,Debian 13,纯 CPU,qwen2.5:1.5b。Responses API,即 OpenAI 兼容端,Codex 及同类客户端使用的接口。第一轮让模型记住一个它不可能猜到的东西:

POST /v1/responses
{"model": "qwen2.5:1.5b",
 "input": "My secret word is PINEAPPLE. Remember it. Reply with just OK."}

→ 200  status: completed   input_tokens: 45   "OK"

第二轮,按照 API 文档的方式,通过引用上一次响应来继续对话:

{"model": "qwen2.5:1.5b",
 "previous_response_id": "resp_578667",
 "input": "What is my secret word? Reply with just the word."}

→ 200  status: completed   error: null   input_tokens: 41   "password"

password。来看两组对照。同样的问题,把完整历史记录放在请求体里发送:

input: [user: "My secret word is PINEAPPLE…", assistant: "OK", user: "What is my secret word?…"]

→ 200   input_tokens: 68   "PINEAPPLE"

再看同样问题,完全不带历史,只有裸的第二轮文本:

input: "What is my secret word? Reply with just the word."

→ 200   input_tokens: 41   "password"

使用 previous_response_id 的请求和不带历史的请求消耗了相同数量的 41 个输入 token,产生了同样离谱的猜测。模型两次看到的是同样的东西:只有问题,没有别的。

同样的情况在 /api/codex/v1/responses 上也成立,这是 ollama launch codex 为 Codex Desktop 建立的代理路径。41 个 token,一个猜测(这次是 Qwen),完成。

为什么这个问题容易被忽略

没有任何地方报错。响应不是错误,不是未完成,不是空响应。它是一个格式正确的完成响应,里面还有一个看似合理的答案。发送 previous_response_id 的对话客户端每一轮都能收到回复;只是回复针对的对话和它以为的完全不同——每次都是全新的对话。

在工具调用循环里,问题表现得更具体、更严重。第一轮:模型发出 function_call。客户端运行工具后,按照协议规定只发回这些——previous_response_id 加上 function_call_output:

轮次 1   "Call the check tool once, then reply exactly DONE."   → function_call    input_tokens 144
轮次 2   previous_response_id + function_call_output "test passed"
         → 200  completed  "The test passed successfully! Is there anything
           else you need help with?"                              input_tokens 144
对照组    完整历史,无 previous_response_id
         → 200  completed  "DONE"                                 input_tokens 179

第二轮回复友好、相关,但错了:指令要求说 DONE,模型根本没看到。模型看到的是一个凭空出现的工具结果,然后尽力发挥。这就是本地模型版本的表现。引发这个问题的上游报告是通过同一个 Codex 代理访问一个托管的 :cloud 模型,对同样的裸 function_call_output 得到的是空的 output_text,且 input_tokens: 0——Codex Desktop 随后将其视为一个干净结束的轮次。在本地模型上这个确切症状没有复现,我也没有测试托管模型。真正复现出来的是底层问题:上下文丢了,后端对没有上下文的工具结果如何处理,完全取决于后端。

实际发生了什么

openai/responses.go,Ollama 0.34.0。请求类型:

type ResponsesRequest struct {
    Model        string
    Input        json.RawMessage
    Instructions *string
    Tools        []ResponsesTool
    ...
    // no PreviousResponseID field
}

Go 的 encoding/json 会忽略没有匹配字段的键,除非设置了 DisallowUnknownFields。没有设置。所以 previous_response_id 在解码阶段就被丢弃了,任何 handler 都看不到它。

而响应类型倒是认识这个字段:

PreviousResponseID *string `json:"previous_response_id"`
...
PreviousResponseID: nil, // Not supported

所以每条响应都带着 "previous_response_id": null。这是整个交互中唯一诚实的信号,然而没有任何客户端会回头读取这个字段——OpenAI 的实现会回显你发送的 id,所以根本没什么可检查的。实现这个功能的功能请求 ollama/ollama#15954 自今年五月就开放至今。

关于 Codex 路径的一条注记

因为很容易搞错。/api/codex/v1/responses 不是一个独立的实现。它是一个路由器:请求中指定的模型如果出现在 ~/.codex/ollama-launch-codex-routing.json 里,就转发给 Ollama 自己的 /v1/responses;其他模型则转发给 OpenAI。在一个没有路由文件的新安装上,该端点返回 503 read Codex Ollama model catalog … no such file,这不是 bug,只是 ollama launch codex 还没运行过。自己手动写一行目录文件,代理就会把本地模型当作托管模型一样路由——而 previous_response_id 也同样会被丢弃,因为丢弃发生在下游。

Ollama 里没有任何机制恢复状态,所以客户端必须停止期待它。每一轮都发送完整对话:

{"model": "qwen2.5:1.5b",
 "input": [
   {"role": "user", "content": "…轮次 1…"},
   {"role": "assistant", "content": "…轮次 1 回复…"},
   {"type": "function_call", "call_id": "call_x", "name": "check", "arguments": "{}"},
   {"type": "function_call_output", "call_id": "call_x", "output": "test passed"}
 ]}

这就是上面消耗 68 token / 179 token 的对照组,在两条路径上都有效。大多数 OpenAI 兼容客户端本来就是这样做的;没有这样做的那些是针对 Responses API 的有状态模式构建的——Codex 是其中最突出的代表,所以上游报告来自 Codex Desktop。

在信任一个 Responses 服务器的状态之前先验证它。只需要四个请求:

import json, secrets, urllib.request
URL, MODEL = "http://127.0.0.1:11434/v1/responses", "qwen2.5:1.5b"
def post(b):
    r = urllib.request.Request(URL, data=json.dumps(b).encode(), headers={"content-type":"application/json"})
    return json.load(urllib.request.urlopen(r))
def text(d): return " ".join(c["text"] for o in d["output"] if o["type"]=="message" for c in o["content"])
word = "".join(secrets.choice("BCDFGHJKLMNPQRSTVWXZ") for _ in range(7))
plant, ask = f"My secret word is {word}. Reply with just OK.", "What is my secret word? Reply with just the word."
r1 = post({"model": MODEL, "input": plant, "stream": False})
r2 = post({"model": MODEL, "previous_response_id": r1["id"], "input": ask, "stream": False})
r4 = post({"model": MODEL, "input": ask, "stream": False})
print(word, "| via id:", r2["usage"]["input_tokens"], repr(text(r2)), "| no history:", r4["usage"]["input_tokens"], repr(text(r4)))
LNLPFVF | via id: 41 'Unlimited possibilities' | no history: 41 'Password'

如果带 id 的请求和不带历史的请求消耗相同 token 数,且都不知道那个词,那么服务器就没有在维护状态,不管 status 字段怎么说。工具包里的 check-responses-state.sh 在此基础上还跑了完整历史记录的对照组,所以太小的模型记不住这个词会被报告为 unknown,而不是 pass 或 fail。

可以推广的习惯

窄一点说:completed 状态描述的是服务器处理的请求,而不是你发送的请求。这里服务器处理的是"我的秘密词是什么?",并完美完成了它。previous_response_id 没有被拒绝,没有收到警告,也没有收到 incomplete_details 通知。等到有任何地方可以投诉的时候,它已经不在了。

宽一点说:这是一个零成本的对照实验。这次诊断里,token 数量做了所有工作。两个本应携带不同量级上下文的请求消耗了相同的 token,而另一个本应与第一个携带相同上下文的请求却消耗了更多。那个数字在每条响应里都有,而且是精确的,不依赖于对模型输出的解读。当一个有状态的 API 可能其实无状态时,先数 token,再读文字。这和固定 prompt、移除约束、看字节是否变化是同一套动作:找到那个如果功能真实现了就一定会变的数字,然后观察它有没有动。

本文的修复已作为经过测试、可直接运行的脚本收录在工具包里。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
Grok 4.8 训练完成:2.5 万亿参数
下一篇
Bengio 深度解析 AI Agent 作弊机制