1. 从一次工具调用失败说起:Qwen-Agent 的 ReAct 与 Function Call 到底怎么选
Qwen-Agent 是阿里通义千问团队开源的一套 Agent 开发框架,它把大模型的指令遵循、工具调用、规划、记忆能力封装成可复用的组件,让你用几十行代码就能搭出一个能读文档、能调工具、能多跳推理的智能体。它适合谁?适合已经会写 Python、想快速验证 RAG 增强 Agent 的开发者,也适合手里有一堆本地文档、想让模型自己决定“先查什么、再算什么”的工程同学。
我最初接触它是因为一个很具体的需求:手头有一批 PDF 报告,想让模型回答跨文档的对比问题。直接塞进上下文会超长,纯向量检索又经常召回不准。Qwen-Agent 给出的答案是把 Agent 分成三个复杂度级别——Lv1 检索、Lv2 分块阅读、Lv3 逐步推理,每一层都建立在前一层之上。这个分层思路很实用,因为它让你可以按问题难度选择成本。
但真正上手时,第一个卡点往往不是框架本身,而是模型服务怎么接。Qwen-Agent 默认走 DashScope,也支持 OpenAI 兼容接口。如果你本地跑的是 vLLM 或 Ollama,或者想用一个统一的 Key 管理多个模型通道,就需要一个稳定的 API 入口。我实测下来,用 TaoToken 的统一通道接 Qwen-Agent 是可行的,Base URL 填https://taotoken.net/api,Key 用平台生成的,Model ID 按你实际调用的模型填。这样 ReAct 和 Function Call 两种模式都能跑通,后面我会给出完整配置。
ReAct 和 Function Call 的区别,用一句话说:ReAct 是让模型“边想边说”,把推理过程写成文本,框架再去解析动作;Function Call 是模型直接输出结构化的函数调用参数,框架按 schema 执行。前者对模型能力要求低、调试直观,后者更稳定、更适合生产。Qwen-Agent 两种都支持,你可以在同一个 Assistant 里混用。下面从环境准备开始,一步步把链路跑通。
2. TaoToken 前置准备:统一 Key 与 API 通道接入 Qwen-Agent 的配置方法
在写 Agent 代码之前,先把模型通道准备好。Qwen-Agent 的llm_cfg支持两种写法:一种是model_server: 'dashscope',走 DashScope 原生协议;另一种是 OpenAI 兼容模式,填model_server为 base_url。后者通用性更强,vLLM、Ollama、以及 TaoToken 的统一通道都走这个模式。
你需要准备三样东西:Base URL、API Key、Model ID。Base URL 用https://taotoken.net/api,注意这里不带任何查询参数。API Key 在控制台的 API Keys 页面生成,生成后复制保存,页面关闭后不再完整显示。Model ID 取决于你要调用的模型,比如qwen-max、qwen-plus,或者你本地部署后映射到通道上的模型名。
环境变量方式是最省事的,避免把 Key 写进代码提交到仓库。在终端里执行:
export TAOTOKEN_API_KEY="sk-你的key" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你用.env文件管理,记得把.env加进.gitignore。Windows PowerShell 下用$env:TAOTOKEN_API_KEY="sk-...",CMD 下用set TAOTOKEN_API_KEY=sk-...。这一步看起来简单,但后面 401 报错十有八九是环境变量没生效或者拼写错了。
安装 Qwen-Agent 本身:
pip install -U qwen-agent如果你要用code_interpreter工具,它依赖 Docker 或本地 Python 执行环境,建议先确认docker命令可用。如果只是跑 ReAct 文本推理和自定义工具,不装 Docker 也能跑。另外json5是自定义工具里解析参数的常用库,pip install json5一并装上。
配置模板我建议单独放一个llm_config.py,这样多个 Agent 脚本可以复用:
import os llm_cfg = { 'model': 'qwen-max', 'model_server': os.getenv('TAOTOKEN_BASE_URL', 'https://taotoken.net/api'), 'api_key': os.getenv('TAOTOKEN_API_KEY'), 'generate_cfg': { 'top_p': 0.8, 'temperature': 0.5, } }这里有个细节:Qwen-Agent 在 OpenAI 兼容模式下,model_server填的是 base_url,框架会自动拼接/chat/completions。如果你填成https://taotoken.net/api/v1反而可能重复。实测填https://taotoken.net/api即可。Model ID 必须和通道上可用的模型一致,填错会返回模型不存在的错误。
注意:不要把 API Key 硬编码在
llm_cfg里然后提交到公开仓库。用环境变量或本地配置文件,是成本最低的安全习惯。
准备好之后,先别急着写复杂 Agent,用一段最小代码验证通道是否通。下一节给出可复制的配置和验证脚本。
3. 可复制配置:ReAct 与 Function Call 双模式 Agent 模板
这一节给两份可直接跑的模板。第一份是 Function Call 模式,用自定义工具my_image_gen演示结构化调用;第二份是 ReAct 模式,用Assistant的默认推理循环演示文本式工具选择。两份都走 TaoToken 通道。
先看 Function Call 模板。核心是继承BaseTool并注册,description和parameters决定了模型能不能正确调用:
import os import json5 import urllib.parse from qwen_agent.agents import Assistant from qwen_agent.tools.base import BaseTool, register_tool @register_tool('my_image_gen') class MyImageGen(BaseTool): description = 'AI 绘画服务,输入文本描述,返回图像 URL。' parameters = [{ 'name': 'prompt', 'type': 'string', 'description': '期望的图像内容描述', 'required': True }] def call(self, params: str, **kwargs) -> str: prompt = json5.loads(params)['prompt'] prompt = urllib.parse.quote(prompt) return json5.dumps( {'image_url': f'https://image.pollinations.ai/prompt/{prompt}'}, ensure_ascii=False ) llm_cfg = { 'model': 'qwen-max', 'model_server': os.getenv('TAOTOKEN_BASE_URL', 'https://taotoken.net/api'), 'api_key': os.getenv('TAOTOKEN_API_KEY'), 'generate_cfg': {'top_p': 0.8} } system_instruction = '''你是一个乐于助人的AI助手。 收到用户请求后,先绘制图像得到 URL,再用中文回复用户。''' bot = Assistant( llm=llm_cfg, system_message=system_instruction, function_list=['my_image_gen'] ) messages = [] query = '画一只在草地上奔跑的柯基' messages.append({'role': 'user', 'content': query}) response = [] for response in bot.run(messages=messages): pass print(response[0]['content'])这段代码里,function_list只放了my_image_gen,模型会在需要时输出函数调用参数,框架解析后执行call。如果你把code_interpreter也加进去,模型可能会选择写代码而不是直接调工具,这取决于 system message 的引导。
再看 ReAct 模式。ReAct 的关键是让模型在文本里显式写出“思考—行动—观察”的循环。Qwen-Agent 的Assistant在function_list非空时会自动进入工具调用循环,但你可以通过 system message 引导它先输出推理文本:
system_instruction = '''你是一个善于逐步推理的助手。 面对复杂问题时,先写出你的思考过程,再决定调用哪个工具。 可用工具:my_image_gen。 每次只调用一个工具,观察结果后再继续。''' bot = Assistant( llm=llm_cfg, system_message=system_instruction, function_list=['my_image_gen'] )ReAct 和 Function Call 在 Qwen-Agent 里并不是互斥的。底层都是bot.run()返回一个生成器,每次 yield 的是当前累积的消息列表。你可以通过打印response[0]['content']看到模型是输出了文本推理,还是输出了function_call字段。实测下来,qwen-max在 Function Call 模式下更稳定,qwen-plus在 ReAct 文本推理下响应更快、成本更低。
如果你要接 RAG,把检索结果作为files或 system message 的一部分传进去即可。Qwen-Agent 的Assistant支持files参数,内部会做分块和检索。对于 Lv2 分块阅读,框架会并行评估每个块的相关性;对于 Lv3 逐步推理,它会把 Lv2 封装成工具供上层调用。你不需要自己实现这套逻辑,但需要理解它的成本——每个块都要过一次模型,块多了费用会上去。
提示:先用小文档(几页 PDF)验证链路,再换大文档。大文档下 Lv2 的并行调用会明显变慢,建议加超时和重试。
配置写好后,下一步是验证请求是否真的成功,以及成功结果长什么样。
4. 验证请求与成功结果:一次完整的工具调用链路测试
验证分三步:通道连通性、单工具调用、多轮工具链。第一步用最小请求确认 Key 和 Base URL 没问题:
from openai import OpenAI import os client = OpenAI( api_key=os.getenv('TAOTOKEN_API_KEY'), base_url=os.getenv('TAOTOKEN_BASE_URL', 'https://taotoken.net/api') ) resp = client.chat.completions.create( model='qwen-max', messages=[{'role': 'user', 'content': '回复两个字:通了'}] ) print(resp.choices[0].message.content)如果这里报 401,说明 Key 无效或没读到环境变量;如果报连接错误,检查 Base URL 是否多了/v1或末尾斜杠。这一步通了,再跑 Qwen-Agent 的完整链路。
第二步,跑上一节的my_image_gen模板。成功时你会看到类似输出:
好的,我已经为你生成图像,URL 如下: https://image.pollinations.ai/prompt/...同时终端里可能打印出工具调用的中间消息。Qwen-Agent 的bot.run()每次 yield 的response是一个消息列表,最后一条是模型的最终回复。你可以遍历打印每条消息的role和content,观察工具调用发生在哪一轮。
第三步,测多轮工具链。把 system message 改成需要两步操作的指令,比如“先画图,再描述图中内容”。模型可能会先调my_image_gen,拿到 URL 后再生成描述文本。如果你加了code_interpreter,它甚至可能写代码去下载图片。这一步能跑通,说明 Function Call 的循环机制是正常的。
对于 RAG 场景,验证方式是给Assistant传一个本地 PDF:
bot = Assistant( llm=llm_cfg, system_message='根据文档回答问题,引用原文。', function_list=[], files=['./docs/report.pdf'] )然后问一个文档里明确有答案的问题,看回复是否引用了正确段落。如果模型答非所问,先检查 PDF 是否被正确解析——Qwen-Agent 内部用pdfplumber或类似库提取文本,扫描版 PDF 可能提取为空。这种情况需要先做 OCR,再喂给 Agent。
成功结果的判断标准:工具被正确调用、参数符合 schema、最终回复包含工具返回的信息。如果模型只是“假装”调用了工具,实际没触发call,通常是description写得太模糊,或者parameters的required没设对。把工具描述写具体,比如“输入英文 prompt,返回图片 URL”,模型的选择准确率会明显提升。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 报错对照
这一节按真实报错来。第一个高频错误是 401 Unauthorized。表现是请求直接返回{"error": {"message": "Invalid API key"}}。原因通常有三个:环境变量名拼错、Key 复制时带了空格、或者用了已删除的 Key。排查方法是在 Python 里打印os.getenv('TAOTOKEN_API_KEY')[:8],确认前几位和平台上一致。如果用的是.env文件,确认python-dotenv已加载。
第二个是local proxy failed或连接超时。这类报错通常出现在 Base URL 写错、网络出口受限、或者本地开了某些网络工具导致请求被拦截。先确认https://taotoken.net/api能通,用curl -I测一下。如果公司网络有出口限制,换一个网络环境再试。注意不要在代码里配置任何代理地址,保持直连即可。
第三个是reading 'choices'或KeyError: 'choices'。这通常发生在响应体不是标准 OpenAI 格式时,比如通道返回了错误 JSON,但代码直接取resp.choices。排查方法是先打印原始响应:
import requests r = requests.post( 'https://taotoken.net/api/chat/completions', headers={'Authorization': f'Bearer {os.getenv("TAOTOKEN_API_KEY")}'}, json={'model': 'qwen-max', 'messages': [{'role': 'user', 'content': 'hi'}]} ) print(r.status_code, r.text[:500])如果返回的是{"error": ...},根据错误信息定位。常见的是模型名不存在,把 Model ID 换成通道上确认可用的。
第四个是 OAuth 相关报错,比如OAuth token expired或invalid_grant。如果你用的是某些需要 OAuth 的客户端(比如 Claude Code 类工具),注意 OAuth 和 API Key 是两套认证。Qwen-Agent 走的是 API Key,不需要 OAuth。如果你在 CC Switch、Cline MCP 或 Codex 的auth.json里配置,三件套要写全:Base URL 填https://taotoken.net/api,Key 填生成的 API Key,Model ID 填实际模型名。缺任何一个都会认证失败。
还有一个容易忽略的:code_interpreter报 Docker 相关错误。如果你没装 Docker,或者 Docker 服务没启动,这个工具会失败。解决方法是启动 Docker Desktop,或者把function_list里的code_interpreter去掉,只保留自定义工具。
注意:报错信息里如果出现
proxy字样,先检查是不是环境变量HTTP_PROXY/HTTPS_PROXY被设置了。清掉这两个变量再跑。
排查顺序建议:先测通道连通性,再测单工具,最后测多轮。每步都打印原始响应,不要只看异常堆栈。大部分问题出在配置层,而不是框架层。
6. 语义一致 CTA:把 Qwen-Agent 接入你的日常开发流
链路跑通之后,下一步是把它用起来。如果你主要做模型能力验证和对话测试,可以直接在模型对话页面里试不同 Model ID 的效果,确认哪个模型在你的任务上表现最好。如果你要长期跑编码类 Agent,或者需要稳定的工具调用通道,Coding Plan 更适合,它按周期提供额度,不用每次手动充值。
接入文档里有各语言和各客户端的配置示例,包括 OpenAI SDK、LangChain、以及常见的 IDE 插件。API Keys 页面用来生成和管理 Key,建议给不同项目建不同的 Key,方便排查和吊销。如果你用 Claude Code 类工具做代码润色或重构,注意它需要的是完整的 Base URL + Key + Model ID 三件套,缺一不可。
我自己的习惯是:先用模型对话快速验证 prompt 和工具描述,确认模型能正确选择工具后,再写进 Qwen-Agent 脚本里跑批量任务。这样比直接写代码调试快很多,因为对话页面能实时看到模型的推理过程。对于 RAG 场景,先用小文档在对话里测检索质量,再换大文档跑 Lv2/Lv3,成本可控。
最后留一个实用技巧:Qwen-Agent 的bot.run()返回的是生成器,你可以把每次 yield 的消息落盘成 JSONL,方便回放和对比不同模型的表现。这个日志在调工具描述时特别有用,能清楚看到模型是在哪一轮决定调用哪个工具的。