跑 hello-agents 的 MCP,TaoToken(https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content=)注册后建一把 Key,这一步放在最后做也行,但越早准备越省事。整条链路里最容易翻车的不是 agent 本身,而是05_UseMCPToolInAgent里的LLM_BASE_URL——前面01_TestConnect、02_Connect2MCP、03_GitHubMCP全都顺顺当当,一到 SimpleAgent 把 MCPTool 挂上去,模型通道才真正开始跑,Token 也是从这一刻开始出的。前面那些脚本连的是本地 stdio 子进程,压根不经过模型,所以你在那几步里怎么改.env都看不出区别,真正要动的是最后这个文件的模型地址。
我按原文的推进顺序把整条路重走了一遍,包括中间那个看着吓人的RuntimeError: Event loop is closed。这篇不打算只给一份.env模板就收工,想把每一步的因果讲清楚:为什么npx会找不到、14 个文件系统工具是怎么冒出来的、agent.run("计算 123 + 456")到底把什么塞进了上下文、以及LLM_BASE_URL后面到底该写什么。
1. pip install "hello-agents[protocol]==0.2.2" 之后,先撞上 npx: None
1.1 conda 环境里只装了 Python,Node.js 是缺的
起手式跟原文一样,先建一个干净的 conda 环境再装库,避免污染系统里的 Python:
conda create -n agent python=3.11 -y conda activate agent pip install "hello-agents[protocol]==0.2.2"装完不会有任何异常提示,import也正常,于是很容易误以为环境已经齐了。问题出在第一个真正去连 MCP 服务器的脚本上:MCPClient默认走 stdio 传输,也就是由它自己subprocess起一个子进程,把npx当成可执行文件去拉@modelcontextprotocol/server-filesystem。conda 环境里只有 Python 解释器,没有 Node.js 运行时,这个npx就不存在。
报错信息通常长这样,看着像是在说“npx 等于 None”,其实核心是上一层的FileNotFoundError:
No such file or directory: 'npx' npx: None很多人第一反应是去改脚本里的command参数,把npx换成绝对路径,或者干脆写npx.cmd。方向错了——根因是这台机器没装 Node.js,换成什么路径都没用。先确认一下:
which node which npx两条都返回空,就说明确实缺运行时,接着装就好。
1.2 conda install -n agent -c conda-forge nodejs -y 把 npx 补上
按原文的做法,直接往当前 conda 环境里塞 Node.js,这样node、npx会跟着环境一起激活,不会跑到全局去:
conda install -n agent -c conda-forge nodejs -y node -v npx -v两条版本号都能打出来,npx: None这一类报错就彻底消失了。这里有个细节值得留意:装完 Node.js 之后要确认当前 shell 还在agent环境里,如果你中途开过新终端,记得conda activate agent再跑脚本,否则依然会提示找不到npx。
另外一个坑是npx首次拉包需要联网下载@modelcontextprotocol/server-filesystem,第一次启动会慢十几秒。如果脚本里有超时设置,别把它调得太短,否则你会看到一个莫名其妙的连接超时,而不是缺 Node.js 的报错,排查方向就被带偏了。
2. 01_TestConnect 到 03_GitHubMCP:14 个文件系统工具是怎么列出来的
2.1 MCPClient 的 stdio 启动参数要写对
Node.js 到位之后,01_TestConnect基本就是一次连通性检查。传给MCPClient的关键信息无非三样:启动命令、参数列表、以及允许访问的目录。命令行那部分,概念上等价于:
npx -y @modelcontextprotocol/server-filesystem ./sandbox-y的作用是跳过 npx 的交互式确认,脚本环境下必须加,否则子进程会停在“是否安装该包”的提示上,表现就是脚本卡住不动。后面那个目录参数决定了文件系统服务器能碰哪些路径——它是一个白名单,传./sandbox就只能读./sandbox里的东西,传项目根目录就能读整个项目。这个白名单是 MCP 文件系统服务器的安全边界,不是可选项。
脚本里对应的地方,大致是把命令和参数组成一个列表交给MCPClient,如果你的版本里字段名不一样(有的叫command/args,有的包了一层server_params),照着你本地那份源码里的签名填就行,值本身不变。
2.2 02_Connect2MCP 里那份 14 个工具的清单
02_Connect2MCP做的事比第一个脚本多一步:连上之后调一次列出工具的方法,把服务器暴露的能力打出来。文件系统服务器通常会给出十来个工具,不同版本在数量上会差一两个,常见的有:
read_file:读单个文本文件read_multiple_files:一次读多个write_file、edit_file:写和改create_directory、list_directory、list_directory_with_sizesdirectory_tree:把目录结构整个吐出来move_file、search_filesget_file_info、list_allowed_directories
原文那次跑出来是 14 个。这个数字本身不重要,重要的是你能在输出里看到工具名和入参 schema——它意味着握手成功、tools/list拿到了返回。如果你的输出里工具列表是空的,先别急着怀疑通道问题,检查两件事:一是sandbox目录是否真的存在,二是白名单路径是不是写成了相对路径而脚本的工作目录又变了。
2.3 03_GitHubMCP 顺带跑通说明协议层没问题
03_GitHubMCP是另一个 MCP 服务器,走的是 GitHub 那套工具。它能跑通,等于给前面的结论又加了一层佐证:MCPClient的连接逻辑、stdio 子进程管理、工具发现这一整套都是好的。这一步不需要你换任何模型通道——它调的依然是 MCP 服务器本身,跟LLM_BASE_URL一点关系都没有。
所以到03_GitHubMCP为止,你手上其实已经有了一个判断依据:报错如果出现在这三个脚本里,八成是环境或服务器参数问题;报错出现在后面的 Agent 脚本里,才轮到模型通道背锅。这条分界线后面排障会反复用到。
3. RuntimeError: Event loop is closed 与换通道无关
3.1 这条噪音来自子进程管道的析构
跑完01、02之后,终端里经常会在正常输出下面多出一段:
RuntimeError: Event loop is closed Exception ignored in: <function BaseSubprocessTransport.__del__>它出现在脚本已经打印完结果之后,退出阶段才冒出来。原因是 asyncio 的BaseSubprocessTransport在垃圾回收时试图去关闭子进程的管道,而此时事件循环已经关掉了,于是析构函数抛了个异常;因为发生在__del__里,Python 只能打印成 “Exception ignored”。
它不影响任何业务结果:工具列出来了,文件读到了,函数返回值也对了。原文的处理方式是加一层sys.unraisablehook,把这类特定异常吞掉,让终端干净一点。这一步纯粹是观感问题,不做也不会让程序算错。别把这条报错跟换通道混在一起排查,它在你改.env之前和之后都会出现,属于典型的“看起来吓人、其实无害”。
3.2 sys.unraisablehook 过滤的写法
思路是自定义一个 hook,判断异常是不是那种退出阶段才出现的 asyncio 噪音,是就静默跳过,不是就交回默认处理:
import sys import asyncio _default_hook = sys.unraisablehook def _quiet_hook(unraisable): text = repr(getattr(unraisable, "exc_value", "")) if isinstance(getattr(unraisable, "exc_value", None), RuntimeError) and "Event loop is closed" in text: return _default_hook(unraisable) sys.unraisablehook = _quiet_hook放在脚本入口最前面即可,注意别把整个RuntimeError类别一刀切,Event loop is closed这个字符串还是要匹配一下,不然真出了别的运行时错误你也会看不见。
4. 05_UseMCPToolInAgent:SimpleAgent 一挂上 MCPTool 就开始花 Token
4.1 agent.run("计算 123 + 456") 背后发生了什么
前三个脚本都是“连服务器、列工具”,05_UseMCPToolInAgent是第一次让模型参与进来:把 MCPTool 交给 SimpleAgent,然后跑一句agent.run("计算 123 + 456")。这一句看着像小学算术,链路却是完整的:
- 这句用户输入被拼进对话上下文,随请求发给模型;
- 模型在可用的工具列表里挑,发现有个内置的
add,于是返回一个工具调用意图; - Agent 框架执行
add(123, 456),拿到579; - 这个结果会被塞回上下文,再发一次请求给模型,让模型组织出自然语言答复。
第 2 步和第 4 步都是真实的模型调用,也就是两次消耗。如果 MCPTool 里挂了文件系统服务器,同样的流程会再多几轮:模型决定调read_file,框架执行,把文件内容塞回上下文,模型再回复。所以文件一大,上下文就跟着涨。这就是为什么前面几步不花钱、这一步开始花钱——真正发请求的是HelloAgentsLLM,不是 MCP 服务器。
4.2 HelloAgentsLLM 只认 .env 里的 LLM_API_KEY 和 LLM_BASE_URL
HelloAgentsLLM读配置的方式很朴素:从环境变量里取LLM_API_KEY和LLM_BASE_URL,项目里一般由.env加python-dotenv在启动时加载。也就是说,你想让这条模型通道接哪儿,改的就是这两行,改脚本、改 MCPTool 的配置都是白费劲。
这也是最容易被忽略的一点:.env往往放在脚本目录下,而你可能在别的目录里用 IDE 打开项目、从别的地方执行脚本,结果.env根本没被加载,程序用的是系统环境变量或者干脆回落到某个默认地址。表现就是“我明明改了,怎么还报鉴权失败”。跑之前先确认工作目录:在脚本目录下执行,或者显式指定env文件路径。
5. 把 LLM_BASE_URL 指到 TaoToken:.env 的写法与两个坑
5.1 先在 TaoToken 落地页建一把 Key
打开 TaoToken 注册,进控制台创建一把 API Key,复制出来。这把 Key 就是.env里LLM_API_KEY的值。它跟 MCP 没关系,MCP 服务器不需要任何 Key,需要 Key 的只有模型通道这一层。
建议按项目分钥匙:一个项目一把 Key,团队里几台机器各建各的,别几个人共用一把。后面要是哪台机器配置写错了疯狂重试,或者某个项目要停掉,你在控制台能直接定位到是哪把 Key 在动。
5.2 .env 完整示例:LLM_BASE_URL 用 https://taotoken.net/api
回到脚本目录,打开或者新建.env,把两行改掉:
LLM_API_KEY=YOUR_API_KEY LLM_BASE_URL=https://taotoken.net/api如果你的 hello-agents 版本还需要指定模型名,再加一行,值从模型广场拿:
LLM_MODEL_NAME=以模型广场当时列表里的 ID 为准这里有两个高频错误,都是顺手多写造成的。第一个是在 Base URL 后面加/v1:https://taotoken.net/api/v1这种写法会让路径拼接后变成/api/v1/chat/completions之类的双段地址,直接 404。填进去的就是https://taotoken.net/api,末尾不要/v1,也不要多余的斜杠。第二个是把官网首页当成接口地址:https://taotoken.net/?utm_source=...那一串是给人点的落地页,浏览器能打开不代表接口能用,往里面 POST 请求只会拿到一个 HTML 页面。人的页面和机器调用的地址是两回事,.env里只写https://taotoken.net/api。
改完.env记得别把它提交进 Git。这个文件里放的是明文 Key,进仓库等于公开。
5.3 模型 ID 一律以模型广场当时列表为准
模型名这一项别凭记忆写。不同接入方对同一个模型的命名习惯不一样,有的带前缀,有的带日期后缀,抄别人博客里的字符串大概率对不上。以 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 上的模型广场当时列出来的 ID 为准,复制粘贴,不要手打。
选哪个模型取决于你要干什么。这里的任务是把工具调用结果翻译成人话,属于典型的轻量对话加结构化输出,不需要挑最贵的那个。先拿一个便宜快速的试通链路,确认add能被正确调用,再按需要往上换。先用小模型验证通路,再用大模型跑真实任务,这个顺序能帮你省掉不少排查时间。
6. 验证:add 工具被自动调用、filesystem 工具能读文件、请求落在 TaoToken
6.1 先确认 hello-agents 装全
在改完.env之后、跑 Agent 之前,先用一行命令确认库本身没问题:
python -c "from hello_agents.tools import MCPTool, A2ATool, ANPTool; print('ok')"打印出ok,说明protocol那部分依赖装齐了。这一步很值,因为它把“库没装全”和“配置写错”两种可能提前分开了;如果这里就报ImportError,那后面所有排查都没意义,先回去补装。
6.2 跑 05_UseMCPToolInAgent,看两件事
python 05_UseMCPToolInAgent.py输出里要盯两处。第一处是内置add工具被自动调用了,模型返回的最终答复里应该出现579,而且中间能看到工具调用的痕迹——这说明工具注册和自动调用这条链路是通的。第二处是文件系统那边的外部工具确实能读文件:让 agent 去读sandbox里的某个文本文件,输出里能看到文件内容,说明 MCP 服务器挂载成功。
第三件事得换个地方看:回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 的控制台,看这次调用的用量记录有没有新增。如果请求确实打到了这边,用量会往上走;如果记录纹丝不动而程序也没报错,那多半是你的.env压根没被加载,程序用的还是老配置。这一条比看日志直观得多。
6.3 多台机器各建各的 Key,Base URL 保持同一个
团队协作时的常见做法是:每台机器、每个开发者各建一把 Key,LLM_API_KEY各不相同,而LLM_BASE_URL全部写成同一行https://taotoken.net/api。这样出问题时能按 Key 定位到人,改起配置来又不用记不同的地址。把这两行抽成模板发给同事,比在群里贴截图靠谱。
还有一点要注意:LLM_BASE_URL是全局生效的,同一个 shell 里如果有别的项目也在用LLM_*系列环境变量,会互相串。跑之前echo $LLM_BASE_URL看一眼当前实际生效的值,比对着.env干猜快得多。
7. 本篇三个报错对照与下一步
7.1 报错对照表
| 现象 | 出现的脚本 | 根因 | 处理 |
|---|---|---|---|
No such file or directory: 'npx'、npx: None | 01_TestConnect | 环境里没有 Node.js 运行时 | conda install -n agent -c conda-forge nodejs -y后npx -v验证 |
Exception ignored ... Event loop is closed | 02_Connect2MCP退出阶段 | 子进程管道在事件循环关闭后析构 | sys.unraisablehook过滤,与通道无关 |
| 404 或返回 HTML 页面 | 05_UseMCPToolInAgent | LLM_BASE_URL多了/v1,或误填了官网首页 | 改回https://taotoken.net/api |
这张表的价值在于定位顺序:先看报错出现在哪个脚本,再往上套。第一类永远先查 Node.js,别去动.env;第二类直接忽略;只有第三类才需要回头检查 Key 和 Base URL。很多人在第一步就冲去改配置,白白绕一大圈。
7.2 下一步:从模型对话到 Coding Plan
链路跑通之后,建议用一个更直接的场景再验一次配置,那就是 模型对话,用跟.env里同一把 Key 发条消息,如果这边通、Agent 那边不通,问题就锁定在文件加载或环境变量上,而不是 Key 本身。如果你打算把 MCP 这条路继续往深里做,日常调用量会上来,可以顺手看看 Coding Plan 是否够用;需要再建新 Key 或者给同事开一把,去 控制台 API Keys 就行。要是你同时也在折腾 Claude Code 的接入,环境变量对照可以看 接入文档,那套变量名跟 hello-agents 的LLM_*不是一回事,别混用。
回头看,这整条链路真正需要你手动配置的其实只有.env里那两行。Node.js 是环境问题,Event loop is closed是观感问题,14 个工具是协议层正常的输出,只有HelloAgentsLLM读的那两行决定了请求发去哪。把这两行改对、把/v1那个尾巴忍住不加,剩下的就是让 agent 自己去调工具了。