
1. 从一次真实踩坑说起Node.js 里 function call 为什么总差点意思先说我遇到的具体问题。前段时间我在做一个 Node.js 的文档处理小工具需求很朴素用户丢进来一个 PDF 或者 Excel让模型读一下然后按固定格式输出摘要。我一开始走的是最传统的 function call 路线——在请求里塞 tools 数组每个工具写清楚 name、description、parameters模型返回 tool_calls我再去执行对应函数。跑是能跑但很快就撞墙了。第一工具一多prompt 里的 tools 定义就膨胀得厉害光描述就吃掉一大截上下文第二每加一个能力我都要在 Node.js 侧重新写一遍 schema、写一遍执行逻辑、再写一遍错误处理重复劳动特别多第三也是最烦的模型有时候会在不该调用工具的时候调用或者把参数拼错我得在业务代码里加一堆兜底判断。后来我把目光转向了 skills 这套思路。简单说skills 就是让模型在合适的时候临时学会一项新能力而不是把所有能力说明书一次性全塞给它。它和 MCP、function call 不是替代关系而是互补MCP 解决的是“工具怎么标准化暴露”function call 解决的是“模型怎么触发调用”而 skills 解决的是“能力怎么按需加载、渐进式披露”。这篇就聚焦 Node.js 环境用 opencode 配合 MCP把 function call 这条链路真正打通一次。适合谁看如果你已经会写 Node.js想让模型调用你自己的函数又不想被臃肿的 tools 定义拖死那这篇就是给你准备的。我会给出可复制的 MCP 服务配置片段、function call 注册示例以及本地验证步骤让你在自有项目里跑通完整链路。核心检索词先摆出来skills 编排、opencode、Node.js、MCP、function call。这几个词会贯穿全文你照着做就能落地。2. 前置准备TaoToken 接入与 opencode 环境搭建在动手写代码之前得先把模型调用这条线接好。我这里用的是 TaoToken 作为模型接入层它的好处是接口统一Node.js 侧不用为不同模型改来改去。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 端点是 https://taotoken.net/api 注意 API 地址后面不加 UTM 参数配置的时候别搞混。先说 Node.js 环境。我本地用的是 Node 20 LTS建议你别用太老的版本因为后面 MCP 的 SDK 对 ESM 和 fetch 有要求。装好之后建一个空目录初始化项目mkdir opencode-mcp-demo cd opencode-mcp-demo npm init -y npm install modelcontextprotocol/sdk这里装的是 MCP 的官方 SDKNode.js 侧写 MCP 服务端和客户端都要用它。装完检查一下 package.json把type: module加上因为 SDK 的示例基本都是 ESM 写法混用 CommonJS 容易出幺蛾子。接下来是 opencode。opencode 是一个开源的 AI 编程代理终端里跑支持 Plan 和 Build 两种模式切换。Plan 模式只做规划不动文件Build 模式直接执行修改。安装方式很简单去官网下载对应平台的包或者用包管理器装。装完之后在终端运行opencode启动第一次会让你选模型和连 API key。这里有个关键点opencode 的配置文件默认在C:\Users\你的用户名\.config\opencodeWindows或者~/.config/opencodemacOS/Linux。你要在这个目录下建一个skill文件夹后面放 skills 的 markdown 文件。skills 的本质就是一堆带元数据的 md 文件模型按需加载不用的时候不占上下文。模型接入这块在 opencode 里用/connect命令配置 API keyBase URL 填https://taotoken.net/apiModel ID 根据你要用的模型填。如果你不确定用哪个模型可以先在模型对话里试一下https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 。确认模型能正常返回之后再回到 opencode 里配。配好之后opencode 就能识别你项目里的 skills 了。但注意skills 本身不负责执行外部函数它负责的是“告诉模型什么时候该干什么”。真正执行函数还得靠 MCP 把工具暴露出来再通过 function call 触发。这就是为什么我要把这三者串起来讲。顺便提一句如果你打算长期在编码和 Agent 场景里用可以考虑 Coding Plan路径是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 它更适合这种持续调用的场景。API Keys 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 遇到配置问题先去文档里翻。3. 可复制配置MCP 服务端 function call 注册完整片段这一节是全文的核心我直接把能跑的配置和代码贴出来。你照着复制改改路径就能用。先建 MCP 服务端。在项目根目录建server.jsimport { Server } from modelcontextprotocol/sdk/server/index.js; import { StdioServerTransport } from modelcontextprotocol/sdk/server/stdio.js; import { CallToolRequestSchema, ListToolsRequestSchema, } from modelcontextprotocol/sdk/types.js; const server new Server( { name: demo-mcp-server, version: 1.0.0 }, { capabilities: { tools: {} } } ); // 注册工具列表 server.setRequestHandler(ListToolsRequestSchema, async () { return { tools: [ { name: read_pdf_summary, description: 读取本地 PDF 文件并返回摘要输入文件路径, inputSchema: { type: object, properties: { filePath: { type: string, description: PDF 文件的绝对路径 }, }, required: [filePath], }, }, ], }; }); // 处理工具调用 server.setRequestHandler(CallToolRequestSchema, async (request) { const { name, arguments: args } request.params; if (name read_pdf_summary) { const { filePath } args; // 这里替换成你真实的 PDF 解析逻辑 return { content: [ { type: text, text: 已读取文件${filePath}摘要内容为示例文本。, }, ], }; } throw new Error(未知工具${name}); }); const transport new StdioServerTransport(); await server.connect(transport);这段代码做了两件事一是通过ListToolsRequestSchema把工具暴露出去模型能看到read_pdf_summary这个名字和它的参数 schema二是通过CallToolRequestSchema接收调用请求执行实际逻辑。注意inputSchema就是 function call 里的 parameters 定义模型靠它来拼参数。然后配置 opencode 识别这个 MCP 服务。在~/.config/opencode下建opencode.json或者你项目里的配置文件加上{ mcp: { demo-server: { command: node, args: [/绝对路径/server.js], env: {} } } }这个 JSON 片段就是 MCP 服务的注册配置。command是启动命令args是参数路径一定要写绝对路径相对路径在 opencode 里经常找不到。配好之后重启 opencode它会在启动时拉起这个 MCP 服务并通过 stdio 通信。接下来是 skills 的 markdown 文件。在~/.config/opencode/skill下建一个文件夹比如pdf-summary里面放SKILL.md--- name: pdf-summary description: 当用户需要读取 PDF 并生成摘要时使用此 skill --- # PDF 摘要 Skill ## 触发条件 用户提到 PDF、文档摘要、读取文件内容时触发。 ## 执行步骤 1. 确认用户提供的文件路径存在 2. 调用 MCP 工具 read_pdf_summary 3. 将返回的摘要内容整理后输出给用户 ## 注意事项 - 文件路径必须是绝对路径 - 如果工具返回错误提示用户检查文件是否存在这个 md 文件就是 skills 的载体。模型可见的只有 frontmatter 里的 name 和 description具体执行步骤在任务命中时才动态加载进上下文。这就是所谓的懒加载、渐进式披露能省下大量 token。三件套到这里就齐了Base URL 是https://taotoken.net/apiKey 在 API Keys 页面拿Model ID 按你选的填。MCP 负责暴露工具skills 负责编排触发时机function call 负责实际调用。三者配合链路才完整。4. 本地验证从请求到成功返回的完整过程配置写完得验证它真的能跑。我分两步走先单独验证 MCP 服务端再通过 opencode 走完整链路。第一步单独测 MCP 服务端。MCP 用的是 stdio 通信直接跑node server.js会卡住等输入这是正常的。更靠谱的方式是写一个简单的客户端测试脚本client-test.jsimport { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; const transport new StdioClientTransport({ command: node, args: [./server.js], }); const client new Client( { name: test-client, version: 1.0.0 }, { capabilities: {} } ); await client.connect(transport); const tools await client.listTools(); console.log(可用工具, JSON.stringify(tools, null, 2)); const result await client.callTool({ name: read_pdf_summary, arguments: { filePath: /tmp/demo.pdf }, }); console.log(调用结果, JSON.stringify(result, null, 2)); await client.close();跑node client-test.js如果看到工具列表里有read_pdf_summary并且调用返回了摘要文本说明 MCP 服务端没问题。这一步能帮你排除掉大部分配置错误比如路径写错、SDK 版本不匹配。第二步在 opencode 里走完整链路。启动 opencode切到 Build 模式输入类似这样的指令帮我读取 /tmp/demo.pdf 并生成摘要opencode 会先匹配 skills发现pdf-summary的 description 命中于是加载SKILL.md的详细步骤。然后模型根据步骤里的指示决定调用 MCP 工具read_pdf_summary通过 function call 把filePath参数传过去。MCP 服务端执行完返回结果模型再整理成自然语言输出。如果一切正常你会在终端看到类似这样的返回已读取文件/tmp/demo.pdf摘要内容为示例文本。到这里一次完整的 skills 编排 MCP function call 链路就跑通了。整个过程里模型并没有一次性拿到所有工具的完整说明而是先看到 skills 的名称和描述命中后才加载细节再触发具体的 function call。这就是按需加载的价值。实测下来这套组合在工具数量多的时候优势特别明显。以前我把十个工具的 schema 全塞进 prompt光描述就上千 token现在模型先看 skills 列表只有命中的那个才展开上下文压力小很多。5. 常见报错排查401、local proxy failed、reading choices 怎么解链路跑通之前大概率会撞几个报错。我把踩过的坑列出来你对照着排查。401 Unauthorized。这个最常见基本是 API key 没配对或者过期了。检查 opencode 里/connect配的 key 是不是从 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 拿的Base URL 是不是https://taotoken.net/api。注意 Base URL 后面不要多加斜杠或者路径有些客户端会拼错。如果 key 没问题还是 401去接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 确认一下当前模型的调用格式。local proxy failed。这个报错通常出现在 MCP 服务启动失败的时候。opencode 尝试拉起server.js但进程没起来或者立刻退出了。排查顺序先手动跑node server.js看有没有语法错误再检查opencode.json里的args路径是不是绝对路径最后确认 Node.js 版本太老的版本不支持 ESM 的 top-level await会直接报错退出。reading choices 相关报错。这个一般出现在模型返回结构不符合预期的时候比如你用的模型返回格式和 OpenAI 兼容格式有差异。检查 Model ID 是不是填对了有些模型需要特定的调用参数。如果反复出现先在模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里单独测一下这个模型能不能正常返回排除是模型本身的问题。OAuth 相关报错。如果你在 opencode 里用了需要 OAuth 的模型接入方式可能会遇到 token 刷新失败。这种情况建议改用 API key 直连的方式Base URL 填https://taotoken.net/apiKey 填你申请的 keyModel ID 填对应模型。三件套齐全基本不会出 OAuth 问题。还有一个隐蔽的坑skills 的SKILL.md里 frontmatter 格式写错。name和description必须用---包起来少一个横线模型就识别不到skill 不会触发。我一开始就因为这个排查了半天以为是 MCP 的问题其实是 md 文件格式错了。另外如果你用的是 Claude Code 这类工具做润色或者接入配置逻辑是一样的Base URL、Key、Model ID 三件套缺一不可。Claude Code 的接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 照着配就行。别跳过配置步骤直接说“连上后就能用”那样大概率跑不起来。6. 继续深入把 skills 编排用到你自己的项目里链路跑通只是起点。真正有价值的是把这套模式搬到自己的业务里。我给你几个可以马上动手的方向。第一个方向把重复的 function call 逻辑抽成 MCP 工具。比如你项目里经常要查数据库、调内部 API、读本地文件这些都可以写成独立的 MCP 工具注册一次所有支持 MCP 的客户端都能用。不用再为每个 AI 应用重复写 tool 定义这就是 MCP 解决“工具不能共享”问题的核心价值。第二个方向用 skills 做任务编排。一个复杂任务往往需要多个步骤比如“读取 PDF → 提取关键信息 → 写入数据库 → 生成报告”。你可以为每个步骤写一个 skill模型按需加载逐步执行。这比把所有逻辑塞进一个巨型 prompt 要清晰得多也省 token。skills 本质上是 sub-agent 的一种轻量实现便于传输和共享。第三个方向结合 Coding Plan 做长期自动化。如果你要搭自动选题、自动内容创作这类全流程系统持续调用模型是常态。Coding Plan 路径是 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 适合这种场景。配合 opencode 的 Plan/Build 模式切换Plan 阶段做规划Build 阶段执行整个流程能跑得很顺。最后提醒一点MCP 工具不要直连生产数据库。我见过有人图省事把生产库的连接直接写进 MCP 服务结果模型一个误调用就出大事。正确做法是加一层只读接口或者沙箱环境工具只暴露必要的操作。安全边界一定要在架构层面划清楚别指望模型每次都听话。这套组合我用了几个月最大的感受是skills 负责“什么时候做什么”MCP 负责“能做什么”function call 负责“实际怎么做”。三者各司其职Node.js 侧只需要维护好 MCP 服务端和 skills 的 md 文件剩下的交给模型编排。你可以先从一个小工具开始试跑通之后再逐步扩展。