1. 为什么工具增强型智能体总在“最后一公里”翻车
Function Calling 和 MCP(Model Context Protocol)这两个词,最近在智能体圈子里出现频率极高。简单说,Function Calling 是让模型知道“该用哪个工具”,MCP 是让所有工具遵循“同一套交互语言”。前者解决单次调用,后者解决跨应用、跨平台的工具复用。适合谁?适合已经跑通 Qwen-Agent 基础对话、想让智能体真正“动手做事”的开发者,也适合被各种工具接入配置折磨过的工程同学。
我见过太多项目卡在同一个地方:模型能正确输出函数名和参数,但工具执行结果回传后模型不认;或者本地 stdio 跑得好好的,一换到 HTTP/SSE 传输就超时。问题往往不在模型本身,而在配置链路——API Key 通道、工具描述格式、传输层参数三者没对齐。这篇就聚焦 Qwen-Agent 框架下,从 Function Calling 到 MCP 的配置演进,给出可复制的 settings.json 与 config.toml 骨架,并用 TaoToken 统一 Key/API 通道接入,最后附验证动作确认工具调用链路真的生效。
2. TaoToken 前置:统一 Key 与 API 通道
在动手写工具之前,先把模型通道固定下来。Qwen-Agent 默认走 DashScope 的兼容接口,但如果你同时要接多个模型、或者想让 MCP Server 和 Agent 共用一套凭证,用 TaoToken 做统一入口会省掉很多重复配置。
TaoToken 的定位是模型 API 聚合通道,官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 基址是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数,直接写进配置即可。
你需要先拿到一个 Key。登录后进入控制台,在 API Keys 页面创建一个新 Key,复制出来。这个 Key 同时用于 Qwen-Agent 的 LLM 调用和后续 MCP Server 里可能用到的模型请求,避免每个组件单独配一套凭证。
注意:Key 只显示一次,建议创建后立刻写入本地配置文件,不要硬编码在代码里提交到仓库。
配置时有两个关键参数要对齐:model字段填你实际要用的模型名,api_key填 TaoToken 的 Key,base_url填https://taotoken.net/api。Qwen-Agent 的Assistant类接受一个llm字典,把这三项塞进去就行。
3. 可复制配置:settings.json 与 config.toml 骨架
3.1 settings.json:Qwen-Agent 侧配置
Qwen-Agent 本身没有强制的 settings.json 规范,但工程上建议把 LLM 配置和工具配置分离。下面这个骨架可以直接用:
{ "llm": { "model": "qwen-max", "api_key": "sk-your-taotoken-key", "base_url": "https://taotoken.net/api", "timeout": 60, "max_retries": 2 }, "agent": { "system_message": "你是一个工具增强型助手,优先调用可用工具获取实时信息。", "function_list": ["search_tickets", "book_ticket"], "max_tool_calls": 5 }, "mcp": { "enabled": true, "servers": [ { "name": "travel-assistant", "transport": "stdio", "command": "python", "args": ["mcp_travel_server.py"] } ] } }这里function_list里写的是工具名,实际注册时用BaseTool子类实例。mcp.servers数组是给支持 MCP 的客户端用的,Qwen-Agent 原生不直接读这个字段,但你可以自己写一层适配,把 MCP Server 暴露的工具转成BaseTool注册进去。
3.2 config.toml:MCP Server 侧配置
MCP Server 如果用 Python SDK 写,建议把传输方式和工具开关放在 config.toml 里,避免每次改代码:
[server] name = "travel-assistant" version = "1.0.0" [transport] type = "stdio" [llm] api_key = "sk-your-taotoken-key" base_url = "https://taotoken.net/api" model = "qwen-max" [tools.search_attractions] enabled = true max_results = 10 [tools.generate_itinerary] enabled = true max_days = 10读取时用tomllib(Python 3.11+)或tomli:
import tomllib with open("config.toml", "rb") as f: config = tomllib.load(f) api_key = config["llm"]["api_key"] base_url = config["llm"]["base_url"]这样 MCP Server 内部如果要调模型做摘要或规划,也能复用同一套 TaoToken 通道,不用再单独配环境变量。
3.3 工具定义与注册
Function Calling 的核心是工具描述要准确。以门票助手为例,两个工具的定义如下:
from qwen_agent.tools import BaseTool class SearchTickets(BaseTool): description = '搜索指定景区在指定日期的可用门票' parameters = { 'type': 'object', 'properties': { 'scenic_spot': {'type': 'string', 'description': '景区名称'}, 'date': {'type': 'string', 'description': '游玩日期,格式YYYY-MM-DD'} }, 'required': ['scenic_spot'] } def call(self, params: dict, **kwargs): spot = params.get('scenic_spot') date = params.get('date', '任意日期') mock = {'故宫': {'成人': 60, '学生': 30}, '长城': {'成人': 40, '学生': 20}} if spot in mock: return f"{spot}门票:{mock[spot]},日期:{date},余票充足" return f"未找到{spot}的门票信息"注册时把实例放进function_list:
from qwen_agent.agents import Assistant bot = Assistant( llm={ 'model': 'qwen-max', 'api_key': 'sk-your-taotoken-key', 'base_url': 'https://taotoken.net/api' }, function_list=[SearchTickets()], system_message='你是门票助手,帮助用户查询和预订门票。' )跑起来后,用户问“故宫明天有票吗”,模型会输出search_tickets调用请求,Qwen-Agent 自动执行call方法并把结果回传,模型再生成最终回答。
4. 验证请求:确认工具调用链路生效
配置写完不代表链路通了。你需要一个可观测的验证动作,确认三件事:模型确实发起了函数调用、工具确实被执行、结果确实回传给了模型。
4.1 打印中间消息
Qwen-Agent 的run方法返回的是消息列表流。把每一步都打出来:
messages = [{'role': 'user', 'content': '故宫明天有票吗?'}] for response in bot.run(messages): for msg in response: print(f"[{msg.get('role')}] {msg.get('content', '')[:200]}") if msg.get('function_call'): print(f" -> 函数调用: {msg['function_call']['name']}") print(f" -> 参数: {msg['function_call']['arguments']}")如果链路正常,你会看到类似输出:
[user] 故宫明天有票吗? [assistant] -> 函数调用: search_tickets -> 参数: {"scenic_spot": "故宫", "date": "2025-01-16"} [function] 故宫门票:{'成人': 60, '学生': 30},日期:2025-01-16,余票充足 [assistant] 故宫明天有票,成人票60元,学生票30元,余票充足。4.2 用 curl 直接验证 TaoToken 通道
如果模型侧没反应,先排除通道问题。用 curl 打一次 TaoToken 的 API:
curl -X POST https://taotoken.net/api/v1/chat/completions \ -H "Authorization: Bearer sk-your-taotoken-key" \ -H "Content-Type: application/json" \ -d '{ "model": "qwen-max", "messages": [{"role": "user", "content": "你好"}] }'返回里有choices[0].message.content就说明 Key 和通道没问题。如果返回 401,检查 Key 是否复制完整;返回 404,检查 base_url 是否写成了带路径的形式。
4.3 MCP Server 单独启动验证
MCP Server 用 stdio 传输时,可以先用命令行手动喂一条 JSON-RPC 请求:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | python mcp_travel_server.py正常会返回工具列表的 JSON。如果卡住不动,多半是stdio_server没正确进入事件循环,检查asyncio.run(main())是否在__main__里调用。
5. 本篇常见错排查
5.1 模型不调用工具,只输出文本
最常见的原因是工具描述不够具体。description要写清楚“什么时候用”,而不是“这是什么”。比如搜索指定景区在指定日期的可用门票比门票搜索工具好得多。另外parameters里的required字段要准确,缺了必填参数模型可能直接放弃调用。
5.2 函数调用参数解析失败
Qwen-Agent 内部用 JSON 解析arguments。如果模型输出的参数里带了中文引号或多余空格,解析会报错。可以在call方法里加一层容错:
import json def call(self, params: dict, **kwargs): if isinstance(params, str): params = json.loads(params) # 后续逻辑5.3 MCP Server 启动后客户端连不上
stdio 传输要求 Server 进程和 Client 进程在同一台机器上,且 Server 不能往 stdout 打非 JSON-RPC 的日志。如果你在代码里用了print调试,会污染协议流。把调试信息写到 stderr:
import sys print("debug info", file=sys.stderr)5.4 TaoToken 返回 429
并发请求过多会触发限流。在llm配置里加max_retries和退避策略,或者降低 Agent 的max_tool_calls,避免一次对话里连续打太多请求。
5.5 工具执行结果回传后模型“失忆”
有些模型对function角色的消息支持不完整。确认你用的模型在 TaoToken 通道上支持 function role。如果不支持,可以把工具结果包装成user消息追加到对话里,并在 system message 里说明“工具结果会以 user 消息形式返回”。
6. 从 Function Calling 到 MCP 的下一步
Function Calling 适合单一 Agent 内部的轻量工具调用,5 到 20 个工具规模下开发复杂度低、上手快。MCP 适合构建可复用的工具生态,一次开发多端使用,但需要理解协议规范和传输层配置。实际项目里两者不冲突:应用内专用工具用 Function Calling 快速集成,跨应用复用的工具封装成 MCP Server,Agent 通过 MCP Client 连多个 Server 实现能力组合。
如果你已经跑通了上面的门票助手,下一步可以把SearchTickets和BookTicket抽出来做成独立的 MCP Server,然后在 Qwen-Agent 里写一个适配层,把 MCP 的tools/list结果转成BaseTool子类动态注册。这样你的工具库就能同时被 Claude Desktop、Cursor 和其他支持 MCP 的客户端复用。
需要长期跑编码类 Agent 的话,可以了解下 Coding Plan 的额度方案;只是想先验证模型对话和工具调用是否通,直接开模型对话页面试几条请求最快。接入文档里有完整的参数说明和错误码对照,排障时对着查比盲猜省时间。