
1. 问题现场Agent 多轮工具调用后LLM 开始“照着历史学坏”如果你在做 Agent 开发尤其是带多轮工具调用的那种大概率遇到过一种很别扭的 bug前几轮工具调用都正常聊到第五、第六轮模型突然不按 schema 走了而是在content里手写一段类似tool_call.../tool_call的文本或者写成[Previous assistant tool use] Tool: exec Input: {...}这种“伪调用记录”然后停在那里等你去解析。你以为它在调用工具其实它只是在“模仿历史消息的格式”。这个现象在 Codex 相关的 issue 里被讨论得很细Agent 把历史的工具调用记录压成字符串塞进上下文格式是tool_call ...{...}/tool_call。LLM 看到上下文里反复出现这种结构就误以为“只要我在 content 里写这个格式就能触发工具调用”。于是它不再走真正的 tool_calls 字段而是在自然语言输出里复刻这套格式。更麻烦的是你把格式改成更自然的[Previous assistant tool use] Tool: exec Input: {...}它照样学照样在 content 里写。这说明根因不在格式本身而在于工具调用记录和普通文本被混在同一个 content 字符串里。这篇就围绕这个场景把最小复现、日志定位、settings.json/config.toml骨架以及用 TaoToken 统一 Key 接入的配置串起来。适合正在写 Agent 循环、被历史消息污染坑过的同学也适合想先把接入层跑通再慢慢调 prompt 的人。2. 前置用 TaoToken 统一 Key 把模型接入层先固定下来排查这类问题最怕变量太多一会儿换模型、一会儿换 base_url、一会儿 key 过期最后分不清是 Agent 逻辑错了还是接入层抖了。我的做法是先把接入层固定成一个统一入口再专心看上下文拼装。TaoToken 在这里的角色就是统一 Key 和统一 base_url官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。你需要先拿到一个可用的 Key入口在 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 。拿到之后不管是 OpenAI 兼容的 SDK还是 Codex 这类工具都指向同一个 base_url模型名按你实际要验证的填。这样后面复现“历史消息污染”时模型侧是稳定的问题只可能出在 Agent 的上下文拼装。如果你只是想先确认模型本身会不会被这种格式带偏可以先用模型对话页面手动喂几轮带tool_call的历史观察它下一轮会不会模仿https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。这一步能帮你快速区分“模型天生爱模仿”还是“你的上下文结构有问题”。3. 可复制配置settings.json 与 config.toml 骨架下面这套骨架的目标是让工具调用记录以结构化字段存放而不是压成字符串塞进 content。配置本身不解决逻辑但它能保证你复现时环境一致。先看一个最小settings.json用于 Agent 侧读取模型接入信息{ model: { provider: openai-compatible, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model_name: gpt-4o-mini, timeout_seconds: 60 }, agent: { max_turns: 8, tool_call_history_mode: structured, strip_legacy_tool_tags: true }, logging: { dump_context: true, context_dump_path: ./logs/context_dump.jsonl } }关键字段是tool_call_history_mode。设成structured表示历史工具调用以独立字段存放设成string就是老做法会把它压进 content方便你对比复现。strip_legacy_tool_tags打开后Agent 在拼上下文前会扫描并剥离 content 里残留的tool_call文本避免二次污染。再看 Codex 侧常用的config.toml骨架[model_providers.taotoken] name taotoken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY [profiles.agent-debug] model_provider taotoken model gpt-4o-mini context_window 128000 [agent] tool_call_history_mode structured dump_context true环境变量这样设export TAOTOKEN_API_KEY你的Key注意base_url只写到/api不要自己拼/v1/chat/completionsSDK 会补。如果你用的是 Codex 的 coding 场景长期跑建议走 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 额度模型更省心。4. 最小复现与日志验证把污染点钉在 turn_response 上复现的核心是构造一个“历史里带工具调用记录”的多轮上下文然后看模型下一轮输出。下面这段 Python 用 OpenAI 兼容 SDK 直连 TaoToken手动拼一个被污染的历史import os, json from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keyos.environ[TAOTOKEN_API_KEY], ) # 模拟被污染的历史工具调用被压成字符串塞进 content polluted_history [ {role: user, content: 帮我查一下当前目录文件}, {role: assistant, content: tool_call name\exec\{\cmd\:\ls\}/tool_call}, {role: tool, content: a.py b.py config.toml}, {role: user, content: 再查一次}, ] resp client.chat.completions.create( modelgpt-4o-mini, messagespolluted_history, tools[{ type: function, function: { name: exec, parameters: {type: object, properties: {cmd: {type: string}}}, }, }], ) msg resp.choices[0].message print(content:, msg.content) print(tool_calls:, msg.tool_calls)跑几次你会看到两种结果有时tool_calls正常有时content里直接出现tool_call ...文本tool_calls为空。这就是“LLM 误学历史格式”的现场。接着打开dump_context看./logs/context_dump.jsonl重点找turn_response阶段拼出来的ContextMessage。老结构长这样type ContextMessage struct { Role string json:role Content string json:content }工具调用被压进Content为了区分才写成 XML。修复方向就是让工具调用走独立字段和Content分离。验证动作很简单修复后重新跑上面的脚本连续 10 轮content里不应再出现tool_call或[Previous assistant tool use]这类文本tool_calls应稳定返回。5. 本篇常见错排查报错一401 invalid api key。多半是TAOTOKEN_API_KEY没导出或者base_url写成了带/v1的地址。确认base_url是https://taotoken.net/apikey 从 API Keys 页面重新复制。报错二模型一直不返回tool_calls。先检查tools参数是否传了再检查历史里是否残留tool_call文本。把strip_legacy_tool_tags打开或者手动清洗历史只保留结构化的tool_calls字段。报错三改了自然语言格式后模型还是模仿。这恰好印证根因是“历史格式被学”不是格式本身好坏。别在格式上继续绕直接改上下文结构把工具调用和 content 分离。报错四context_dump.jsonl为空。检查dump_context是否为 true以及进程是否有写权限。日志路径建议用绝对路径避免相对路径在不同工作目录下找不到。报错五Codex 侧配置不生效。config.toml里 profile 名要和启动时指定的--profile一致env_key指向的环境变量必须真实存在。改完重启进程别指望热加载。6. 接入与排障走这两条路别只停在首页如果你现在卡在接入层比如 key、base_url、SDK 报错直接去 API Keys 页面拿 Key再对照接入文档走一遍https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 和 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。文档里有各语言的最小请求示例比在 issue 里翻半天快。如果你是想先确认“模型到底会不会被历史格式带偏”用模型对话页面手动喂几轮最快https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。长期跑 Agent 编码或自动化任务再考虑 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 。把接入层固定住剩下的精力全花在上下文结构上这类“历史消息污染”问题会好定位得多。