最新资讯
stackoverflow.blog 欢迎来到芯片时代:半导体制造、AI 数据中心与芯片设计的未来 www.youtube.com AI Agent 总犯同样的错:CentralAgent AI 将记忆重构为经验系统 pub.towardsai.net Netflix 科学家建议重采样 LLM 评判:5 次仅将误差棒缩小 13% pub.towardsai.net Jev 作为 LLM 前的静默层:构建提示感知的模型路由器 reddit.com OpenAI 称 Astra 仅满足 10% 安全任务标准 dev.to FreeToken:在自有 GPU 上运行前沿 MoE 模型及其内存需求 dev.to 当 AI Agent 说“完成”时,究竟完成了什么? dev.to 部署 Underdog Bark 3.0 Pro:逐字转写、自定义后端与分块设置 dev.to 为艰难对话构建 AI:五个 Agent、一条安全正则与一位盲评监督者 techcrunch.com Palo Alto Networks 洽谈以 5 亿美元收购 AI Agent 安全初创公司 Clutch theinformation.com 黄仁勋被要求就涉华间谍指控向国会作简报 www.infoq.com Edera 推出 Object Capability Bounded Authority,试图将凭证与 AI Agent 工作负载隔离
多元动态
登录 注册
大模型 dev.to · @The Homelab Postmortem

Ollama 的 Responses API 接受 previous_response_id,返回 200 却丢失整个对话

原文 我的收藏

作者在 Ollama 0.34.0、Debian 13、纯 CPU、qwen2.5:1.5b 上测试发现:向 /v1/responses 发送 previous_response_id 会被接受,返回 HTTP 200、status 为 completed、error 为 null,但模型完全没有看到上一轮内容。带该字段的请求与不带历史的请求消耗相同输入 token(41 对 41),回答类型也相同;改为发送完整历史则消耗 68 个 token 且能正常工作。原因在源码:请求结构体没有 previous_response_id 字段,JSON 解码时被丢弃,响应结构体将其置为 nil 并注释 Not supported。作者认为,此前一个托管模型在工具调用后返回空补全的 bug 报告,正是后端在无上下文时收到裸 function_call_output 的表现。

AI推荐理由 作者在 Ollama 0.34.0、Debian 13、CPU、qwen2.5:1.5b 环境下实测:向 /v1/responses 传 previous_response_id 会返回 200 且 status 为 completed、error 为 null,但模型完全看不到上一轮;带该字段与不带历史的输入 token 数相同(41 对 41),而传完整历史为 68 且有效。作者还指出源码中请求结构体没有该字段、响应结构体将其置为 nil 并注释 Not supported。这是对 OpenAI 兼容端点行为的具体、可复现证据,对使用 Codex 类客户端的开发者有直接价值;局限是单机单模型验证,未说明是否已在其他版本或配置复现。

78

正文

TL;DR: send previous_response_id to Ollama's /v1/responses and it is accepted, the reply is HTTP 200 with "status": "completed" and "error": null, and the model has never seen the previous turn. A request that carries previous_response_id costs the same number of input tokens as a request that carries no history — 41 and 41 on this box — and produces the same kind of answer. Sending the full history instead costs 68 and works. The reason is in the source: the request struct has no previous_response_id field, so the JSON decoder throws it away, and the response struct sets it to nil with the comment // Not supported. The bug report that led here, an empty completion from a hosted model after a tool call, is what a bare function_call_output with no context looks like when the backend has nothing to say about it. The symptom Ollama 0.34.0, Debian 13, CPU only, qwen2.5:1.5b. The Responses API, which is the OpenAI-compatible endpoint Codex and similar clients use. Turn 1 plants something the model could not guess: 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" Enter fullscreen mode Exit fullscreen mode Turn 2 continues the conversation the way the API is documented to work, by referencing the previous response: {"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" Enter fullscreen mode Exit fullscreen mode password. Now two controls. The same question with the full history sent in the request body: input: [user: "My secret word is PINEAPPLE…", assistant: "OK", user: "What is my secret word?…"] → 200 input_tokens: 68 "PINEAPPLE" Enter fullscreen mode Exit fullscreen mode And the same question with no history at all, just the bare turn-2 text: input: "What is my secret word? Reply with just the word." → 200 input_tokens: 41 "password" Enter fullscreen mode Exit fullscreen mode The previous_response_id request and the no-history request cost the same 41 input tokens and produce the same guess. The model was shown the same thing both times: nothing but the question. The same holds through /api/codex/v1/responses, the proxy path that ollama launch codex sets up for Codex Desktop. 41 tokens, a guess (Qwen, this time), completed. Why this is easy to miss Nothing fails. The response is not an error, not incomplete, not empty. It is a well-formed completed response with a plausible answer in it. A conversation client that sends previous_response_id gets a reply to every turn; the replies are just to a different conversation than the one it thinks it is having — one that starts fresh every time. With a tool-calling loop the shape is worse and more specific. Turn 1: the model emits a function_call. The client runs the tool and sends back only what the protocol says to send back — previous_response_id plus a function_call_output: turn 1 "Call the check tool once, then reply exactly DONE." → function_call input_tokens 144 turn 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 control full history, no previous_response_id → 200 completed "DONE" input_tokens 179 Enter fullscreen mode Exit fullscreen mode The turn-2 reply is friendly, on-topic, and wrong: the instruction was to say DONE, and the model never saw it. It saw a tool result arrive from nowhere and did its best. That is the local-model version. The upstream report that started this used a hosted :cloud model through the same Codex proxy and got, for the same bare function_call_output, an empty output_text with input_tokens: 0 — which Codex Desktop then read as a cleanly finished turn. That exact symptom did not reproduce here with a local model, and I did not test a hosted one. What did reproduce is the thing underneath it: the context is gone, and what the backend does with a context-free tool result is up to the backend. What is really going on openai/responses.go, Ollama 0.34.0. The request type: type ResponsesRequest struct { Model string Input json.RawMessage Instructions *string Tools []ResponsesTool ... // no PreviousResponseID field } Enter fullscreen mode Exit fullscreen mode Go's encoding/json ignores keys that have no matching field unless DisallowUnknownFields is set. It is not set. previous_response_id is discarded at decode, before any handler sees it. The response type, on the other hand, does know the field exists: PreviousResponseID *string `json:"previous_response_id"` ... PreviousResponseID: nil, // Not supported Enter fullscreen mode Exit fullscreen mode So every response carries "previous_response_id": null. That is the one honest signal in the whole exchange, and it is a field no client reads back — OpenAI's implementation echoes the id you sent, so there is nothing to check. The feature request to implement it, ollama/ollama#15954, has been open since May. A note on the Codex path, because it is easy to get wrong. /api/codex/v1/responses is not a separate implementation. It is a router: a request whose model appears in ~/.codex/ollama-launch-codex-routing.json is forwarded to Ollama's own /v1/responses; any other model is forwarded to OpenAI. On a fresh install with no routing file the endpoint answers 503 read Codex Ollama model catalog … no such file, which is not the bug, it is just ollama launch codex not having run. Write a one-line catalog by hand and the proxy routes a local model exactly as it would a hosted one — and drops previous_response_id exactly the same way, because the drop happens downstream. The fix Nothing in Ollama restores the state, so the client has to stop expecting it to. Send the full conversation on every turn: {"model": "qwen2.5:1.5b", "input": [ {"role": "user", "content": "…turn 1…"}, {"role": "assistant", "content": "…turn 1 reply…"}, {"type": "function_call", "call_id": "call_x", "name": "check", "arguments": "{}"}, {"type": "function_call_output", "call_id": "call_x", "output": "test passed"} ]} Enter fullscreen mode Exit fullscreen mode That is the 68-token / 179-token control above, and it works on both paths. Most OpenAI-compatible clients already do this; the ones that do not are the ones built against the Responses API's stateful mode — Codex being the prominent case, which is why the upstream report came from Codex Desktop. Check before you trust a Responses server with state. It takes four requests: 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))) Enter fullscreen mode Exit fullscreen mode LNLPFVF | via id: 41 'Unlimited possibilities' | no history: 41 'Password' Enter fullscreen mode Exit fullscreen mode If the id-referencing request costs the same as the bare one and does not know the word, the server is not keeping state, whatever the status field says. The toolkit's check-responses-state.sh runs this with a full-history control as well, so a model too small to remember the word is reported as unknown rather than as a pass or a fail. The generalisable habit The narrow one: a status of completed describes the request the server processed, which may not be the request you sent. Here the server processed "what is my secret word?" and completed it perfectly. The previous_response_id was not rejected, not warned about, not incomplete_details-ed. It was not there by the time anything could have complained. The wider one is about controls that cost nothing. The token count did all the work in this diagnosis. Two requests that should have carried different amounts of context cost the same, and one that should have carried the same as the first cost more. That number is in every response, it is exact, and it does not depend on interpreting model output. When a stateful API might be stateless, count the tokens before reading the words. It is the same move as holding the prompt fixed and removing the constraint to see whether the bytes change: find the one number that has to move if the feature is real, and watch whether it moves. Toolkit This post's fix is available as a tested, ready-to-run script in the toolkit. See the toolkit →

来源报道 · 1 篇 · 以下链接均前往原始媒体

← 返回精选动态 原文

更多内容