1. 先搞清楚 Qwen-Code 里 ACP 和 AG-UI 到底谁管谁如果你最近在折腾 Qwen-Code大概率会在文档里同时撞见 ACP 和 AG-UI 这两个词然后陷入一种“这俩是不是一回事”的困惑。我一开始也以为它们是前端视角和后端视角的区别甚至觉得选一个用就行。实际把 qwen-acp 子进程跑起来、抓了几轮报文之后才发现它们根本不是替代关系而是进程内通信和跨网络事件流两层完全不同的东西。先把结论摆出来ACP 是 Qwen-Code 自研的 Agent Client Protocol基于 JSON-RPC 2.0默认走子进程的 stdin/stdout只在本地进程之间传消息不碰网络。AG-UI 是 CopilotKit 那套开源的 Agent-User Interaction Protocol基于 SSE 或 WebSocket专门给浏览器前端消费事件流用的。一个管“宿主程序怎么跟本地 Agent 子进程说话”一个管“Web 前端怎么把 Agent 的流式输出渲染成 UI”。打个比方ACP 像主板上的内部总线AG-UI 像机箱后面的网口。你在本地写个 CLI 脚本调 Qwen-Code只需要 ACP你要做个网页版代码助手浏览器碰不到本地子进程的 stdio就必须在中间加一层协议桥把 AG-UI 的网络事件翻译成 ACP 的 JSON-RPC 报文。这篇文章会带你做三件事第一逐字段对比两套协议的请求/响应结构让你看到报文层面到底差在哪第二给出可复制的 settings.json 和 config.toml 骨架把 TaoToken 的统一 Key 和 API 通道配进去第三教你抓报文、切协议、验证请求是否真的通了。适合正在做 Qwen-Code 本地 CLI 接入、或者准备把它嵌到 Web 前端里的开发者。2. TaoToken 前置配置统一 Key 与 API 通道怎么接在动协议之前得先把模型调用这条链路打通。Qwen-Code 本身是个 Agent 框架它最终还是要调大模型 API 来完成推理。我实测下来用 TaoToken 做统一入口比较省事一个 Key 就能覆盖多个模型通道不用在 Qwen-Code 里到处塞不同厂商的 endpoint。TaoToken 的 API 地址是https://taotoken.net/api注意这个地址不带任何查询参数直接作为 Base URL 用。官网入口在https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content注册完在控制台里生成 API Key。这里有个关键点Qwen-Code 的 ACP 子进程和 AG-UI 后端服务理论上可以共用同一个 TaoToken Key但建议在配置里显式区分环境变量避免本地调试时把生产 Key 带进去。我一般会在项目根目录建一个.env文件里面写TAOTOKEN_API_KEYsk-你的实际key TAOTOKEN_BASE_URLhttps://taotoken.net/api然后在 Qwen-Code 的配置里引用这两个变量。如果你用的是 Claude Code 那套配置习惯~/.claude/settings.json里可以这样写{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的实际key }, model: claude-sonnet-4-20250514 }注意这里的 Base URL 和 Key 是配套的Model ID 要跟你实际在 TaoToken 控制台里开通的通道对应。如果你走的是 OpenAI 兼容格式那就换成OPENAI_BASE_URL和OPENAI_API_KEY地址同样是https://taotoken.net/api。对于 Qwen-Code 自己的config.toml我建议把模型通道单独抽出来[model] provider openai-compatible base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model_id qwen3-coder-plus [acp] transport stdio command qwen-acp args [--config, ./config.toml] [agui] enabled true transport sse port 8787这个骨架里[model]段负责模型调用[acp]段定义子进程怎么启动[agui]段定义 Web 事件流的监听端口。三块分开写的好处是你切协议的时候只动[acp]或[agui]模型通道不用改。如果你用的是 Cline 或者 Roo Code 这类插件配置项名字会不一样但核心三件套不变Base URL 填https://taotoken.net/apiAPI Key 填 TaoToken 生成的 KeyModel ID 填你开通的模型名。Cline 的 MCP 配置里如果出现local proxy failed八成是 Base URL 写成了带路径的完整 endpoint改成纯https://taotoken.net/api就好。3. 可复制配置ACP 与 AG-UI 双协议 settings 骨架这一节直接给能跑的配置。先明确一个原则ACP 的配置核心是“怎么启动子进程”AG-UI 的配置核心是“怎么暴露事件流端口”。两者可以同时开也可以只开一个。先看 ACP 侧的完整config.toml。这个文件放在项目根目录qwen-acp 启动时会读[agent] name qwen-code-local session_dir ./.qwen/sessions max_concurrent_runs 4 [model] provider openai-compatible base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY model_id qwen3-coder-plus temperature 0.2 max_tokens 8192 [acp] transport stdio command qwen-acp args [--config, ./config.toml, --log-level, info] env { TAOTOKEN_API_KEY ${TAOTOKEN_API_KEY} } startup_timeout_ms 15000 [acp.tools] enabled [code_interpreter, file_editor, shell] sandbox true这里[acp]段的transport stdio是关键它告诉宿主程序通过标准输入输出跟子进程通信。env那行把 TaoToken 的 Key 透传给子进程避免子进程读不到环境变量。再看 AG-UI 侧的配置。如果你要在 Web 前端接需要一个后端服务把 ACP 的 JSON-RPC 转成 AG-UI 的 SSE 事件。这个桥接服务的配置可以写在同一个config.toml里也可以单独放一个agui-bridge.toml[server] host 127.0.0.1 port 8787 cors_origins [http://localhost:3000, http://localhost:5173] [agui] transport sse heartbeat_interval_ms 15000 event_buffer_size 256 [bridge] acp_command qwen-acp acp_args [--config, ./config.toml] acp_cwd . reconnect_attempts 3 [bridge.mapping] agent.stream_chunk TEXT_MESSAGE_CONTENT agent.run_complete RUN_FINISHED agent.tool_call TOOL_CALL_START这个桥接配置里[bridge.mapping]段定义了 ACP 方法名到 AG-UI 事件名的映射关系。实际项目里映射逻辑会更复杂但骨架就是这样。如果你用的是 Claude Code 的 settings.json 风格可以这样组织{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的实际key }, model: claude-sonnet-4-20250514, acp: { transport: stdio, command: qwen-acp, args: [--config, ./config.toml] }, agui: { enabled: true, transport: sse, port: 8787 } }注意ANTHROPIC_BASE_URL和ANTHROPIC_API_KEY是给 Claude Code 自身用的Qwen-Code 的 ACP 子进程走的是config.toml里的[model]段。两套配置可以共存但 Key 建议用同一个 TaoToken Key方便统一管理。配置写完之后先别急着跑 Web 前端。用命令行验证 ACP 子进程能不能起来export TAOTOKEN_API_KEYsk-你的实际key qwen-acp --config ./config.toml --log-level debug如果子进程正常启动你会看到它往 stdout 打出一行 JSON-RPC 的初始化消息类似{jsonrpc:2.0,method:agent.ready,params:{sessionId:sess-init,protocolVersion:1.0}}看到这行就说明 ACP 通道通了模型配置也被正确加载了。4. 验证请求抓报文、切协议、看成功结果配置写完只是第一步真正要确认的是报文有没有按预期流动。这一节教你抓 ACP 和 AG-UI 两边的报文并验证协议切换是否生效。先抓 ACP 的报文。最简单的方式是在宿主程序里把子进程的 stdout 重定向到文件同时用tee保留终端输出qwen-acp --config ./config.toml 21 | tee acp-trace.log然后在另一个终端里用一个小脚本往子进程的 stdin 写一条 JSON-RPC 请求。我一般用 Python 快速验证import subprocess, json, time proc subprocess.Popen( [qwen-acp, --config, ./config.toml], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, stderrsubprocess.PIPE, textTrue, bufsize1 ) request { jsonrpc: 2.0, id: 1, method: agent.run, params: { sessionId: sess-verify-001, prompt: 写一个Python快速排序算法, tools: [code_interpreter] } } proc.stdin.write(json.dumps(request) \n) proc.stdin.flush() for _ in range(20): line proc.stdout.readline() if not line: break print(line.strip()) if run_complete in line: break proc.terminate()跑起来之后你应该能看到类似这样的流式输出{jsonrpc:2.0,method:agent.stream_chunk,params:{sessionId:sess-verify-001,content:def quick_sort(arr):\n if len(arr) 1:\n return arr}} {jsonrpc:2.0,method:agent.stream_chunk,params:{sessionId:sess-verify-001,content:\n pivot arr[len(arr) // 2]}} {jsonrpc:2.0,method:agent.run_complete,params:{sessionId:sess-verify-001}}每条stream_chunk都带sessionIdrun_complete没有id字段因为它是通知类消息。这就是 ACP 的典型报文结构请求有id流式推送和完成通知没有id。再验证 AG-UI 侧。启动桥接服务agui-bridge --config ./agui-bridge.toml然后用 curl 订阅 SSE 流curl -N -H Accept: text/event-stream \ http://127.0.0.1:8787/agui/stream?sessionIdsess-verify-002在另一个终端触发一次 Agent 运行curl -X POST http://127.0.0.1:8787/agui/run \ -H Content-Type: application/json \ -d {sessionId:sess-verify-002,prompt:写一个Python快速排序算法}你应该在 SSE 终端看到这样的事件序列event: RUN_STARTED data: {runId:run-abc123} event: TEXT_MESSAGE_START data: {messageId:msg-001,role:assistant} event: TEXT_MESSAGE_CONTENT data: {content:def quick_sort(arr):,messageId:msg-001} event: TOOL_CALL_START data: {toolName:code_interpreter,runId:run-abc123,toolCallId:tool-001} event: TEXT_MESSAGE_END data: {messageId:msg-001,stopReason:finish} event: RUN_FINISHED data: {runId:run-abc123}对比一下就能看出差异ACP 的报文是 JSON-RPC 格式有jsonrpc、method、params字段AG-UI 是 SSE 事件格式有event和data两行事件名是RUN_STARTED、TEXT_MESSAGE_CONTENT这种生命周期语义。验证协议切换是否生效最简单的办法是改config.toml里的[agui] enabled字段从true改成false重启桥接服务再 curl 一次 SSE 端点。如果返回 404 或者连接被拒绝说明 AG-UI 通道确实被关掉了ACP 子进程不受影响。反过来把[acp] transport从stdio改成tcp如果版本支持再跑一次 Python 验证脚本看报文是不是从 socket 而不是 stdin/stdout 出来。我踩过的一个坑是AG-UI 的 SSE 事件里TEXT_MESSAGE_CONTENT的content字段是增量文本不是完整消息。前端渲染的时候要自己拼接不能每次覆盖。ACP 的stream_chunk也是增量但字段名是content语义一样。如果你在桥接层做映射记得把增量语义保留别在中间做聚合。5. 常见报错排查401、local proxy failed、reading choices、OAuth这一节列几个我实际遇到过的报错以及对应的排查路径。每个报错都跟协议配置或 TaoToken 通道有关不是泛泛的“网络问题”。401 Unauthorized这个最常见。ACP 子进程报 401说明[model]段里的api_key_env指向的环境变量没读到。检查两点第一export TAOTOKEN_API_KEYsk-xxx有没有在启动子进程的同一个 shell 里执行第二config.toml里env { TAOTOKEN_API_KEY ${TAOTOKEN_API_KEY} }这行有没有写对${}是 TOML 的变量插值语法不是 shell 的。如果用的是 settings.json检查ANTHROPIC_API_KEY或OPENAI_API_KEY有没有拼错。local proxy failed这个报错通常出现在 Cline 或 Roo Code 的 MCP 配置里。原因是 Base URL 填成了带路径的完整 endpoint比如https://taotoken.net/api/v1/chat/completions。正确做法是只填https://taotoken.net/api让客户端自己拼路径。另外检查一下有没有多余的尾部斜杠https://taotoken.net/api/和https://taotoken.net/api在某些客户端里行为不一样。reading choices 报错这个一般出现在 OpenAI 兼容通道的响应解析阶段。报错信息类似cannot read property choices of undefined。原因是模型返回的 JSON 结构跟客户端预期的不一致。排查步骤先用 curl 直接打 TaoToken 的 API看返回体里有没有choices字段curl -s https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的实际key \ -H Content-Type: application/json \ -d {model:qwen3-coder-plus,messages:[{role:user,content:hi}]} | jq .choices如果 curl 能拿到choices但 Qwen-Code 里报错那就是客户端把响应包了一层或者 Model ID 写错了导致路由到了不兼容的通道。检查config.toml里的model_id是否跟 TaoToken 控制台里开通的通道名完全一致。OAuth 相关报错如果你在 Claude Code 里看到 OAuth 报错比如OAuth token expired或invalid_grant说明客户端在尝试走 OAuth 流程而不是 API Key。解决办法是在 settings.json 里显式配置ANTHROPIC_API_KEY并且确保没有同时存在 OAuth 相关的配置项。Claude Code 的auth.json里如果残留了旧的 OAuth token可以删掉或者重命名为auth.json.bak让它重新走 API Key 认证。还有一个容易忽略的点ACP 子进程的startup_timeout_ms设得太短会导致子进程还没初始化完就被宿主杀掉报错信息可能是subprocess exited with code 1但没有具体原因。把startup_timeout_ms从 5000 调到 15000再跑一次通常就能看到真正的错误输出了。6. 选型建议与接入入口回到最初的问题到底该用 ACP 还是 AG-UI我的判断逻辑是这样的。如果你做的是本地 CLI 工具、VS Code 插件、或者桌面客户端Agent 逻辑跑在本地子进程里不需要浏览器渲染那就只用 ACP。配置简单没有网络开销子进程崩溃也不影响宿主。config.toml里[agui] enabled false桥接服务都不用起。如果你做的是 Web 前端Agent 逻辑跟 HTTP 服务同进程没有子进程隔离需求那就只用 AG-UI。后端直接吐 SSE 事件前端监听TEXT_MESSAGE_CONTENT做打字机效果。这种场景下不需要 qwen-acp 子进程也不需要协议桥。如果你既要 Qwen-Code 的子进程隔离能力又要 Web 前端可视化那就必须双协议配合。链路是浏览器 → AG-UI SSE → 桥接服务 → ACP stdio → qwen-acp 子进程。桥接层负责把agent.stream_chunk映射成TEXT_MESSAGE_CONTENT把agent.run_complete映射成RUN_FINISHED。配置层面TaoToken 的 Key 和 Base URL 在两套协议里是共用的。ACP 侧通过config.toml的[model]段读取AG-UI 侧如果桥接服务也需要调模型就在桥接配置里再引一次环境变量。统一用https://taotoken.net/api作为 Base URLKey 从控制台生成。如果你还没生成 Key可以去控制台创建一个https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。创建完之后在 API Keys 页面复制https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content里面有各客户端的配置示例。想先验证模型通道通不通可以用模型对话页面直接发一条消息https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。如果对话正常返回说明 Key 和 Base URL 没问题再去配 Qwen-Code 的 ACP 或 AG-UI 就不会卡在认证上。长期做编码 Agent 的话Coding Plan 比按量计费更划算https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。Claude Code 用户如果走 Anthropic 兼容通道配置参考https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content。最后提醒一句ACP 和 AG-UI 的报文抓取最好在开发阶段就打开日志--log-level debug会往 stderr 打完整的 JSON-RPC 收发记录。上线前记得把日志级别调回info不然流式输出量大时日志文件会涨得很快。