Ollama 输出带 markdown 解析挂了?TaoToken 通道里重试 LangGraph JSON 调用
2026/9/18 21:26:48 网站建设 项目流程

这篇排障围绕browser_agent.pyollama.chat(format="json")仍返回带 markdown 的文本、json.loads直接抛错展开。模型通道已经切到 TaoToken,官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,Key 也从这里创建;但 TaoToken 只供模型通道,不负责解析 JSON,所以 LangGraph 节点里的重试和清洗逻辑仍要保留。很多浏览器自动化 Agent 在本地用 Ollama 跑得好好的,一换到远程模型通道,第一次就遇到 ```json 包裹、前后解释文本、尾逗号之类的问题,agent_node一执行就报JSONDecodeError。下面按排障顺序,把browser_agent.py从 Ollama 本地通道切到 TaoToken 通道时的配置、重试和验证过程拆开。

原问题与场景:browser_agent.py 里 ollama.chat(format="json") 仍被 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 模式并不总是严格。即使调用时写了format="json",模型仍可能返回:

```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里自己写。

本篇场景就是:模型通道已经走 TaoToken,browser_agent.py里仍保留失败重试逻辑,第二次请求使用从官网创建的 Key,Base URL 填https://taotoken.net/api。目标不是让 TaoToken 保证返回纯 JSON,而是让 LangGraph 的agent_node在遇到带 markdown 的输出时,能通过重试和清洗把steps正常解析出来。

TaoToken 前置:模型通道走 https://taotoken.net/api,Key 从官网创建

先把模型通道从本地 Ollama 切到 TaoToken。打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_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( model="qwen3:4b", messages=[{"role": "user", "content": prompt}], format="json" )

那么切到 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(level=logging.INFO) llm = ChatOpenAI( model=MODEL_ID, api_key=API_KEY, base_url=BASE_URL, temperature=0, timeout=60, max_retries=1, ) 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, flags=re.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(content=prompt)]) 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(content=retry_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()

这段配置的关键点有三个。

第一,ChatOpenAIbase_urlhttps://taotoken.net/apiapi_keyYOUR_API_KEY。如果你的 LangChain 版本参数名不同,也可以用openai_api_key=API_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.invoke

result = app.invoke({ "user_input": "打开 https://example.com,然后截图返回" }) print(result["steps"]) print(result["result"])

成功时,result["steps"]应该是一个列表,里面每个元素都有toolargs。如果第一步goto_url能正常出现,说明agent_node已经从 TaoToken 通道拿到文本,并且完成了 JSON 解析。接着再跑完整 agent,确认execute_node能根据steps调用浏览器工具。只要steps不是空列表,且没有抛JSONDecodeError,这次迁移就算基本成功。

如果你把format="json"从 Ollama 切到 TaoToken 后仍然偶发解析失败,不要急着改浏览器工具,先看raw日志。大多数情况下是模型输出了 markdown 代码块,或者 JSON 前后带了自然语言。重试逻辑加上清洗函数后,应该能覆盖。

本篇常见错排查:json.loads 挂、Base URL 写错与重试无效

下面这些错误在browser_agent.py接入 TaoToken 时很常见,按顺序排查。

  1. 仍然请求本地 Ollama
    代码里还保留ollama.chat,但只改了 Key。这样请求还是会发到http://localhost:11434,TaoToken 通道根本没生效。要么把ollama.chat换成 OpenAI 兼容客户端,要么把本地 Ollama 的 Base URL 改造清楚,不要两边混用。

  2. Base URL 写成https://taotoken.net
    TaoToken 的 API Base URL 是https://taotoken.net/api。少了/api,OpenAI 客户端拼接路径时会 404 或返回不兼容错误。注意 API 地址不加 UTM,直接填https://taotoken.net/api

  3. Key 写成YOUR_API_KEY没替换
    这是最低级但最容易发生的问题。去 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 创建 Key 后,把YOUR_API_KEY替换掉。不要带多余空格,也不要在代码里公开提交。

  4. 模型 ID 不存在或没有权限
    MODEL_ID要和你控制台或文档里的一致。模型 ID 写错时,第一次请求就会失败,根本走不到json.loads。先用 curl 或模型对话验证模型可用,再放进 LangGraph。

  5. 只依赖response_format,不做文本清洗
    有些模型支持 JSON mode,有些模型对response_format支持不完整。即使你在请求里加了 JSON 约束,仍要保留strip_markdown_json。不要假设 TaoToken 会替你解析 JSON,它只负责模型通道。

  6. 重试逻辑没有加“只输出 JSON”
    原场景里,第二次请求必须加“只输出 JSON,不要其他内容”。如果只是原样重试,模型可能再次返回 markdown,导致连续失败。重试 prompt 要明确禁止代码块和解释。

  7. 重试次数没有上限
    如果第二次仍然失败,不要让parse_steps_with_retry无限递归。可以在外层做最多两次解析,或者直接返回空steps并记录错误,避免 Agent 卡死。

  8. 清洗函数把合法 JSON 截坏了
    strip_markdown_json是兜底,不是万能。如果 JSON 内部字符串本身包含{},简单截取可能出错。更稳妥的做法是先用json.loads尝试原文本,再尝试去 markdown 代码块,最后才截取首尾大括号。

  9. steps字段名和 prompt 不一致
    prompt 里要求{"steps": [...]},但模型可能返回{"actions": [...]}{"steps": null}agent_node里用data.get("steps", [])可以避免 KeyError,但最好在 prompt 和解析后都校验一下结构。

  10. 解析逻辑放在工具函数里
    JSON 解析应该在agent_node里完成,而不是散落在浏览器工具中。否则一旦json.loads挂,堆栈会指向工具调用,排查成本变高。保持agent_node负责模型输出解析,execute_node负责执行steps

  11. Playwright MCP 或浏览器工具报错被误判成 JSON 问题
    有时json.loads已经成功,但execute_node里的 selector 失效或超时,导致整个 Agent 失败。这时看日志里steps是否已经打印出来。如果steps正常,问题在浏览器侧,不在 TaoToken 通道。

  12. 本地代理或环境变量干扰
    如果你的机器上有HTTP_PROXYHTTPS_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_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite

需要确认 Base URL、模型 ID、OpenAI 兼容客户端写法时,看接入文档:
https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite

如果你只是想先验证模型当前是否能稳定输出 JSON,可以到模型对话里用同一套 prompt 试几次:
https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite

这套排障的核心不是让 TaoToken 替你解析 JSON,而是把模型通道和 JSON 解析职责分开:TaoToken 负责稳定接入模型,browser_agent.py负责清洗、重试和校验steps。这样 Playwright MCP、Ollama、LangGraph 和浏览器自动化 Agent 的组合在切到远程模型通道后,仍然能保持可控。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询