Ollama 输出带 markdown 解析挂了?TaoToken 通道里重试 LangGraph JSON 调用
这篇排障围绕browser_agent.py里ollama.chat(formatjson)仍返回带 markdown 的文本、json.loads直接抛错展开。模型通道已经切到 TaoToken官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 也从这里创建但 TaoToken 只供模型通道不负责解析 JSON所以 LangGraph 节点里的重试和清洗逻辑仍要保留。很多浏览器自动化 Agent 在本地用 Ollama 跑得好好的一换到远程模型通道第一次就遇到 json 包裹、前后解释文本、尾逗号之类的问题agent_node一执行就报JSONDecodeError。下面按排障顺序把browser_agent.py从 Ollama 本地通道切到 TaoToken 通道时的配置、重试和验证过程拆开。原问题与场景browser_agent.py 里 ollama.chat(formatjson) 仍被 markdown 包裹用 Playwright MCP 和 Ollama 搭浏览器自动化 Agent 时一个很常见的结构是LangGraph 负责流程Playwright MCP 或自己封装的浏览器工具负责执行Ollama 负责根据用户指令生成操作步骤。典型 prompt 会让模型输出{steps: [{tool: goto_url, args: {url: https://example.com}}]}然后agent_node里做json.loads(response[message][content])再把steps交给execute_node执行。问题出在 JSON 模式并不总是严格。即使调用时写了formatjson模型仍可能返回json {steps: [{tool: goto_url, args: {url: https://example.com}}]} 或者更麻烦一点在 JSON 前后加一句“好的以下是操作步骤”和“希望对你有所帮助”。这时候json.loads会直接挂agent_node抛异常整个 LangGraph 流程断掉。原作者的兜底是解析失败后再调一次模型并在 prompt 里加“只输出 JSON不要其他内容”。这个思路是对的但模型通道切到 TaoToken 之后要注意两件事第一第二次请求也要走 TaoToken 的 Key 和 Base URL第二TaoToken 只提供模型通道JSON 解析、markdown 清洗、重试次数控制仍然要在browser_agent.py里自己写。本篇场景就是模型通道已经走 TaoTokenbrowser_agent.py里仍保留失败重试逻辑第二次请求使用从官网创建的 KeyBase URL 填https://taotoken.net/api。目标不是让 TaoToken 保证返回纯 JSON而是让 LangGraph 的agent_node在遇到带 markdown 的输出时能通过重试和清洗把steps正常解析出来。TaoToken 前置模型通道走 https://taotoken.net/apiKey 从官网创建先把模型通道从本地 Ollama 切到 TaoToken。打开 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 在控制台创建 API Key。这里拿到的 Key 就是后面填进 LangGraph 模型客户端的YOUR_API_KEY。注意API Key 不要写进公开仓库先用环境变量或本地.env管理。TaoToken 的 API Base URL 是https://taotoken.net/api这个地址不加 UTM 参数直接作为 OpenAI 兼容客户端的base_url。模型 ID 用你实际选择的MODEL_ID不要照抄示例里的占位符。接入文档里会列出当前可用的模型和推荐参数如果你只是验证 JSON 重试逻辑选一个指令跟随能力较好的模型即可。如果你原来的browser_agent.py用的是import ollama response ollama.chat( modelqwen3:4b, messages[{role: user, content: prompt}], formatjson )那么切到 TaoToken 后应该改成 OpenAI 兼容客户端例如langchain_openai.ChatOpenAI或官方openaiSDK。LangGraph 本身不限定模型客户端只要你的agent_node能拿到文本并解析即可。下面给一套可直接复制的配置保留“第一次解析失败后第二次请求加只输出 JSON”的逻辑。可复制配置LangGraph TaoToken 的 JSON 重试解析新建或修改browser_agent.py把原来的 Ollama 客户端替换为 TaoToken 客户端。下面代码只负责模型通道和 JSON 解析不负责浏览器操作浏览器工具仍可以放在browser_tools.py里。import json import re import logging from typing import TypedDict from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage from langgraph.graph import StateGraph, END # 从 TaoToken 控制台创建并复制 API_KEY YOUR_API_KEY BASE_URL https://taotoken.net/api MODEL_ID YOUR_MODEL_ID logging.basicConfig(levellogging.INFO) llm ChatOpenAI( modelMODEL_ID, api_keyAPI_KEY, base_urlBASE_URL, temperature0, timeout60, max_retries1, ) TOOLS_DESC 可用工具 1. goto_url(url) - 打开网页 2. fill_input(selector, text) - 在输入框填入内容 3. click_button(selector) - 点击按钮 4. get_page_text() - 获取当前页面文本 5. take_screenshot() - 截图返回 base64 6. close_browser() - 关闭浏览器 当你需要操作浏览器时输出 JSON 格式的步骤列表 {steps: [{tool: 工具名, args: {参数名: 参数值}}]} 只输出 JSON不要输出 markdown 代码块不要输出解释。 class BrowserState(TypedDict): user_input: str steps: list result: str def strip_markdown_json(text: str) - str: 尽量把模型返回的文本清洗成纯 JSON。 text text.strip() # 去掉 json 和 text re.sub(r^(?:json)?\s*, , text, flagsre.IGNORECASE) text re.sub(r\s*$, , text) # 如果 JSON 前后还有说明文字截取第一个 { 到最后一个 } start text.find({) end text.rfind(}) if start ! -1 and end ! -1 and end start: text text[start:end 1] return text.strip() def parse_steps_with_retry(prompt: str): 第一次解析失败后第二次请求强制只输出 JSON。 first llm.invoke([HumanMessage(contentprompt)]) raw first.content try: return json.loads(strip_markdown_json(raw)) except json.JSONDecodeError as e: logging.warning(第一次 JSON 解析失败: %s, raw%r, e, raw[:500]) retry_prompt ( prompt \n\n你上一次输出无法被 json.loads 解析。 只输出 JSON不要其他内容不要 markdown 代码块不要解释。 ) # 第二次请求仍然走 TaoToken 通道 second llm.invoke([HumanMessage(contentretry_prompt)]) raw2 second.content return json.loads(strip_markdown_json(raw2)) def agent_node(state: BrowserState): prompt ( f{TOOLS_DESC}\n\n f用户指令{state[user_input]}\n f请输出操作步骤 ) data parse_steps_with_retry(prompt) return {steps: data.get(steps, [])} def execute_node(state: BrowserState): results [] for step in state[steps]: tool step.get(tool) args step.get(args, {}) # 这里按你自己的 browser_tools 导入和调用 # 例如 # if tool goto_url: # results.append(goto_url(**args)) # elif tool fill_input: # results.append(fill_input(**args)) # elif tool click_button: # results.append(click_button(**args)) results.append(f执行 {tool} 参数 {args}) return {result: \n.join(results)} graph StateGraph(BrowserState) graph.add_node(agent, agent_node) graph.add_node(execute, execute_node) graph.add_edge(agent, execute) graph.add_edge(execute, END) graph.set_entry_point(agent) app graph.compile()这段配置的关键点有三个。第一ChatOpenAI的base_url是https://taotoken.net/apiapi_key是YOUR_API_KEY。如果你的 LangChain 版本参数名不同也可以用openai_api_keyAPI_KEY但不要把 Base URL 写成首页地址。第二strip_markdown_json先处理常见的 json 包裹再截取第一个{到最后一个}。这一步能解决大部分“带 markdown 的文本导致json.loads挂”的问题。第三parse_steps_with_retry第一次失败后第二次请求会在原 prompt 后追加“只输出 JSON不要其他内容不要 markdown 代码块不要解释”。第二次请求仍然使用同一个 TaoToken 客户端所以 Key 和 Base URL 会自动复用不需要重新初始化。验证请求与成功结果agent_node 返回的 steps 能被 json.loads 解析配置写完后先不要直接跑完整浏览器任务先用一个最小请求验证 TaoToken 模型通道是否通。可以用 curl 检查curl -sS https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer YOUR_API_KEY \ -H Content-Type: application/json \ -d { model: YOUR_MODEL_ID, messages: [ { role: user, content: 只输出 JSON{\steps\:[{\tool\:\goto_url\,\args\:{\url\:\https://example.com\}}]} } ], temperature: 0 }如果通道正常你应该能看到类似结构{ choices: [ { message: { content: {\steps\:[{\tool\:\goto_url\,\args\:{\url\:\https://example.com\}}]} } } ] }然后回到 Python 里单独调用agent_node或整个app.invokeresult app.invoke({ user_input: 打开 https://example.com然后截图返回 }) print(result[steps]) print(result[result])成功时result[steps]应该是一个列表里面每个元素都有tool和args。如果第一步goto_url能正常出现说明agent_node已经从 TaoToken 通道拿到文本并且完成了 JSON 解析。接着再跑完整 agent确认execute_node能根据steps调用浏览器工具。只要steps不是空列表且没有抛JSONDecodeError这次迁移就算基本成功。如果你把formatjson从 Ollama 切到 TaoToken 后仍然偶发解析失败不要急着改浏览器工具先看raw日志。大多数情况下是模型输出了 markdown 代码块或者 JSON 前后带了自然语言。重试逻辑加上清洗函数后应该能覆盖。本篇常见错排查json.loads 挂、Base URL 写错与重试无效下面这些错误在browser_agent.py接入 TaoToken 时很常见按顺序排查。仍然请求本地 Ollama代码里还保留ollama.chat但只改了 Key。这样请求还是会发到http://localhost:11434TaoToken 通道根本没生效。要么把ollama.chat换成 OpenAI 兼容客户端要么把本地 Ollama 的 Base URL 改造清楚不要两边混用。Base URL 写成https://taotoken.netTaoToken 的 API Base URL 是https://taotoken.net/api。少了/apiOpenAI 客户端拼接路径时会 404 或返回不兼容错误。注意 API 地址不加 UTM直接填https://taotoken.net/api。Key 写成YOUR_API_KEY没替换这是最低级但最容易发生的问题。去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建 Key 后把YOUR_API_KEY替换掉。不要带多余空格也不要在代码里公开提交。模型 ID 不存在或没有权限MODEL_ID要和你控制台或文档里的一致。模型 ID 写错时第一次请求就会失败根本走不到json.loads。先用 curl 或模型对话验证模型可用再放进 LangGraph。只依赖response_format不做文本清洗有些模型支持 JSON mode有些模型对response_format支持不完整。即使你在请求里加了 JSON 约束仍要保留strip_markdown_json。不要假设 TaoToken 会替你解析 JSON它只负责模型通道。重试逻辑没有加“只输出 JSON”原场景里第二次请求必须加“只输出 JSON不要其他内容”。如果只是原样重试模型可能再次返回 markdown导致连续失败。重试 prompt 要明确禁止代码块和解释。重试次数没有上限如果第二次仍然失败不要让parse_steps_with_retry无限递归。可以在外层做最多两次解析或者直接返回空steps并记录错误避免 Agent 卡死。清洗函数把合法 JSON 截坏了strip_markdown_json是兜底不是万能。如果 JSON 内部字符串本身包含{或}简单截取可能出错。更稳妥的做法是先用json.loads尝试原文本再尝试去 markdown 代码块最后才截取首尾大括号。steps字段名和 prompt 不一致prompt 里要求{steps: [...]}但模型可能返回{actions: [...]}或{steps: null}。agent_node里用data.get(steps, [])可以避免 KeyError但最好在 prompt 和解析后都校验一下结构。解析逻辑放在工具函数里JSON 解析应该在agent_node里完成而不是散落在浏览器工具中。否则一旦json.loads挂堆栈会指向工具调用排查成本变高。保持agent_node负责模型输出解析execute_node负责执行steps。Playwright MCP 或浏览器工具报错被误判成 JSON 问题有时json.loads已经成功但execute_node里的 selector 失效或超时导致整个 Agent 失败。这时看日志里steps是否已经打印出来。如果steps正常问题在浏览器侧不在 TaoToken 通道。本地代理或环境变量干扰如果你的机器上有HTTP_PROXY、HTTPS_PROXY或旧 OpenAI 环境变量可能影响ChatOpenAI的请求地址。先在一个干净终端里跑 curl 验证再在 Python 里打印实际base_url。语义一致 CTA排障之后用 API Keys 和接入文档继续跑 Agent如果你已经按上面的方式把browser_agent.py里的模型通道切到 TaoToken并且保留了“第一次json.loads失败后第二次请求加只输出 JSON”的重试逻辑那么接下来要做的就是把这个配置固定下来Key 放进环境变量Base URL 固定为https://taotoken.net/api模型 ID 按项目需求选择。后续再遇到带 markdown 的输出先看raw日志再用清洗函数和重试 prompt 兜底。需要创建或复制 Key 时直接去 API Keys 页面https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite需要确认 Base URL、模型 ID、OpenAI 兼容客户端写法时看接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite如果你只是想先验证模型当前是否能稳定输出 JSON可以到模型对话里用同一套 prompt 试几次https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite这套排障的核心不是让 TaoToken 替你解析 JSON而是把模型通道和 JSON 解析职责分开TaoToken 负责稳定接入模型browser_agent.py负责清洗、重试和校验steps。这样 Playwright MCP、Ollama、LangGraph 和浏览器自动化 Agent 的组合在切到远程模型通道后仍然能保持可控。