n8n-mcp 实战指南:掌握 Custom Code Tool(toolCode)的 LangChain 工具契约,写出 AI Agent 真正会调用的代码
【免费下载链接】n8n-mcpA MCP for Claude Desktop / Claude Code / Windsurf / Cursor to build n8n workflows for you项目地址: https://gitcode.com/GitHub_Trending/n8/n8n-mcp
本文是 n8n-mcp 技能包中 n8n-code-tool 技能 的深度展开,聚焦@n8n/n8n-nodes-langchain.toolCode——n8n AI Agent 可调用的自定义代码工具(Custom Code Tool)。你将理解它与普通 Code 节点的本质区别(“字符串进、字符串出”的 LangChain 契约),掌握非结构化query与结构化 JSON Schema 两种输入模式,学会用工具名与描述做提示词工程,并能够仅凭报错信息快速修复"Wrong output type returned"、"No execution data available"等高频故障。
一、这是 Code Tool,不是 Code 节点
Custom Code Tool 在编辑器里长得和 Code 节点几乎一样——同一个 JavaScript 编辑器、类似的布局——但它来自完全不同的包,遵循完全不同的运行时契约。把它当 Code 节点用,必然踩坑。
| 维度 | Code 节点 | Custom Code Tool |
|---|---|---|
| 节点类型 | n8n-nodes-base.code | @n8n/n8n-nodes-langchain.toolCode |
| 所属包 | n8n-nodes-base | @n8n/n8n-nodes-langchain |
| 调用方 | 上一个节点(工作流流程) | AI Agent(LangChain) |
| 输入 | $input.all()数据流 | query——LLM 传来的字符串或对象 |
| 返回值 | [{json: {...}}](条目数组) | 一个字符串 |
$fromAI() | 不适用 | 不可用(会抛错) |
| HTTP 辅助 | this.helpers.httpRequest | 不暴露给工具沙箱 |
| 状态 | 每次执行的运行数据 | 无getContext、无$getWorkflowStaticData |
一句话概括:Code Tool 是套着 Code 节点外衣的 LangChain 工具,它的契约是“字符串进、字符串出”(string in, string out)。本技能的全部规则都由此派生。
从仓库源码看,这一契约也被校验逻辑所印证:在 src/services/ai-tool-validators.ts 中,validateCodeTool对nodes-langchain.toolCode节点强制执行三项检查——toolDescription缺失报MISSING_TOOL_DESCRIPTION(error 级)、jsCode为空报MISSING_CODE(error 级)、未配置inputSchema/specifyInputSchema则给出 warning 提示。也就是说,工具描述、代码体、输入模式这三件事是本仓库校验器眼中的“硬门槛”。
二、快速开始:最小可用示例
最小 JavaScript Code Tool
// `query` 是 AI 传来的内容(默认是字符串) return `You asked: ${query}`;最小 Python Code Tool
# `_query` 是 AI 传来的内容(默认是字符串),注意下划线前缀 return f"You asked: {_query}"六条铁律
- 必须返回字符串。数字会被自动转换;其他任何类型都会抛出
"The response property should be a string, but it is an object"。 - 输入变量名固定:JS 是
query,Python 是_query,不能改名。 - 严禁在 Code Tool 沙箱中使用
$fromAI()——会抛"No execution data available"。 - 严禁使用
[{json: {...}}]返回格式——那是 Code 节点的契约,会抛"Wrong output type returned"。 - 使用描述性的工具名(字母/数字/下划线,v1.1+)。Agent 通过名字调用工具。
- 编写精确的描述——LLM 依据描述决定是否调用此工具。
三、两种输入模式:非结构化 vs 结构化
Code Tool 的输入形态由specifyInputSchema开关控制。本仓库的 INPUT_SCHEMA.md 对此有完整讲解,这里提炼核心。
模式一:非结构化(默认,specifyInputSchema: false)
AI 以单个字符串作为query传入。如果你需要多个字段,就得让 AI 把它们塞进一个字符串里,由你在代码中解析。实践中,只要描述里写清楚,LLM 会很乐意传一个 JSON 字符串。
// 解析 AI 传来的 JSON 字符串 let params; try { params = typeof query === 'string' ? JSON.parse(query) : query; } catch (e) { throw new Error('Expected a JSON object. Parser said: ' + e.message); } const price = Number(params.price); const months = Number(params.months); // ... return JSON.stringify({ monthly_payment: /* ... */ });- 优点:搭建最简单,只需描述一个字段。
- 缺点:没有 schema 校验——LLM 漏掉字段时,工具只能在运行时抛错。
- 适合:快速原型,以及天然只有一个输入的工具(一个问题、一个 URL、一段文本)。
模式二:结构化(specifyInputSchema: true)
工具变成 LangChain 的DynamicStructuredTool。LLM 看到的是一个带类型的参数 schema,传入的是经过校验的对象作为query,你可以直接访问字段。
// query 现在是匹配 schema 的对象 const price = query.price; const months = query.months; const residual_percent = query.residual_percent; const monthly = computeAnnuity(price, months, residual_percent); return JSON.stringify({ monthly_payment: monthly });Schema 有两种定义方式:
schemaType: "fromJson"+jsonSchemaExample(n8n v≥1.3):粘贴一段示例 JSON,n8n 自动推断 schema,最省事;schemaType: "manual"+inputSchema:自己手写完整 JSON Schema,适合需要字段级description、enum、minimum/maximum约束或可选字段的场景。优点:LLM 获得类型提示,非法调用在代码运行前就被拒绝,代码更干净。
缺点:需要额外配置,且要求 n8n 版本支持 schema(
fromJson需 v≥1.3,节点应设typeVersion: 1.3;旧版本可用manual)。适合:生产环境的多参数工具(计算器、API 封装、任何含数字字段且 LLM 容易把它们字符串化的工具)。
一个关键事实:沙箱收到的始终是{ query }
从 ToolCode 沙箱的绑定方式来看(见 INPUT_SCHEMA.md 中引用的源码片段new JsTaskRunnerSandbox(workflowMode, ctx, undefined, { query })),沙箱恒以{ query }作为输入,区别只在于query装的是什么:
| 模式 | query的类型 | 使用方式 |
|---|---|---|
| 无 schema | string | 需要结构时JSON.parse(query) |
| 有 schema | object(已校验) | 解构:const { price, months } = query; |
Python 同理:无 schema 时_query是字符串,有 schema 时是字典。
模式选择决策树
你的工具需要多于一个输入字段吗? ├─ 否(只是一个 URL、问题、文本块) │ └─ 非结构化——跳过 schema ├─ 是,且字段都是简单类型(数字、布尔、枚举) │ └─ 结构化 + fromJson(最省事) ├─ 是,且需要约束或丰富的字段描述 │ └─ 结构化 + manual └─ 是,且字段复杂 / 要在多个 Agent 间复用 └─ 改用 toolWorkflow(子工作流工具),而不是 toolCode两个 schema 陷阱
- schema 必须是合法的 JSON 字符串:
jsonSchemaExample和inputSchema是“装 JSON 的字符串”而非对象,粘贴进工作流 JSON 时注意转义。节点保存失败或 LLM 看不到字段时,先单独校验 JSON。 - schema 变更不会追溯修复旧的 Agent 运行:如果 Agent 已用非结构化工具启动,切到结构化后系统提示词可能仍反映旧契约。改完 schema 设置后,强制重新运行或重开 Agent 节点。
四、返回值格式:必须是字符串
返回值必须是字符串。LLM 把返回值当作工具的 observation(观察结果)来读取。
// ✅ 字符串 return "42"; // ✅ 数字(n8n 自动转为字符串) return 42; // ✅ 推荐:JSON 编码的结构化结果 return JSON.stringify({ result: 42, currency: "SEK" }); // ❌ 裸对象 → "The response property should be a string, but it is an object" return { result: 42 }; // ❌ 工作流条目格式 → "Wrong output type returned" return [{ json: { result: 42 } }]; // ❌ 数组 → "The response property should be a string, but it is an object" return [1, 2, 3];最佳实践:结构化结果用 JSON.stringify
当工具输出不止一个标量值时,返回 JSON 字符串:
return JSON.stringify({ monthly_payment_sek: 5405, loan_amount: 351920, total_cost_of_credit: 63295 });LLM 解析 JSON 很可靠,并能从中挑选需要的字段呈现给用户。
错误处理:Agent 会读你的失败信息
错误不会只是让工作流停止——它们会回到 LLM 那里,而 LLM 通常会修正调用并重试。善用这一点:
// 方案 A:throw——n8n 会把消息抛给 Agent if (!isFinite(price)) throw new Error('price must be a number, e.g. 439900'); // 方案 B:返回错误字符串——Agent 像读普通结果一样读它 if (!isFinite(price)) return JSON.stringify({ error: 'price must be a number, e.g. 439900' });无论哪种方式,错误信息都要写给 LLM 看:说明哪里错了、合法的调用长什么样。一句裸的throw new Error('invalid input')浪费了一次重试机会;一条有指导性的消息通常能直接修正下一次调用。
注意一个容易被忽略的细节(见 ERROR_PATTERNS.md):数字会被 n8n 自动转字符串,但布尔值不会,需要显式String(someBoolean)或JSON.stringify(someBoolean)。
五、工具名与描述:这是 LLM 看到的工具契约
这两个字段不是文档,而是LLM 眼中的工具契约,请把它们当作提示词工程来对待。
名称(Name)
- 必须匹配
[A-Za-z0-9_]+(v1.1+)。不能有空格、连字符、emoji。 - 用动词化、描述性的名字:
calculate_car_loan、get_weather、search_orders。 - Agent 靠这个名字调用工具。默认的
Code Tool毫无用处——Agent 根本不知道何时该调用它。
描述(Description)
- 说明何时使用、发什么内容。
- 非结构化模式下,给出 LLM 应发送的 JSON 字符串示例。
- 结构化模式下,schema 已说明一切,描述只需说明用途。
非结构化示例(字符串内嵌 JSON 模式):
Determinisktiskt beräknar månadskostnad för billån. Anropa med EN JSON-sträng: {"price":439900,"down_payment":87980,"interest_rate":6.95,"months":36,"residual_percent":50} Fält: price (SEK), down_payment (SEK), interest_rate (% per år), months, residual_percent (0-99).结构化示例(schema 已定义字段):
Deterministically computes the monthly car-loan payment given price, down payment, annual interest rate, term, and residual percent. Use whenever the user asks for monthly cost, total credit cost, or loan breakdown.仓库校验器把“描述缺失”视为 error 而非 warning(MISSING_TOOL_DESCRIPTION,见 ai-tool-validators.ts),再次印证:描述不是可选项,而是工具可被正确调用的前提。
六、高频错误与修复(按报错信息定位)
完整的错误目录(含复现方法)见 ERROR_PATTERNS.md,这里列出七个最典型的失败模式。
错误 1:"Cannot assign to read only property 'name' of object: Error: No execution data available"
- 原因:在 Code Tool 沙箱内调用了
$fromAI()。该辅助函数是为其他工具类节点(HTTP Request Tool、SendGrid Tool、toolWorkflow等)设计的,AI 提供的值通过这些节点的工作流执行数据流动;而 Code Tool 沙箱没有执行数据,输入直接通过query传入。辅助函数抛错后,n8n 尝试给错误的name属性打标注,又因错误对象被冻结而失败,于是出现这条嵌套报错。 - 修复:移除
$fromAI(),直接从query读取(或用specifyInputSchema定义结构化字段)。
// ❌ 错误 const price = $fromAI('price', 'Car price in SEK', 'number'); // ✅ 非结构化——解析 JSON 字符串 const params = JSON.parse(query); const price = Number(params.price); // ✅ 结构化——specifyInputSchema: true const { price } = query;错误 2:"Wrong output type returned"
- 原因:返回了工作流条目格式
[{json: {...}}]。这是普通 Code节点的契约,不是工具契约。 - 修复:返回字符串;结构化输出用
JSON.stringify()。
// ❌ 错误 return [{ json: { monthly_payment: 5405 } }]; // ✅ 正确 return JSON.stringify({ monthly_payment: 5405 });错误 3:"The response property should be a string, but it is an <type>"
- 原因:返回了裸对象、数组,或什么都没返回。
<type>通常是object、undefined、function等。
| 返回的值 | 报错内容 | 修复 |
|---|---|---|
{ result: 42 } | ...is an object | JSON.stringify({ result: 42 }) |
[1, 2, 3] | ...is an object | JSON.stringify([1, 2, 3]) |
(没有return) | ...is an undefined | 补上return |
undefined | ...is an undefined | 返回点东西 |
错误 4:AI 从不调用工具
- 症状:Agent 只凭自身推理回答,执行轨迹里没有任何工具调用。
- 常见原因与修复:
- 名字太泛(
Code Tool、My Tool)→ 改成动词化、领域化的 snake_case:calculate_car_loan、search_orders、lookup_customer。 - 描述没说明触发条件(“Calculates things”太含糊)→ 明确列出应触发工具的用户意图:“Use this whenever the user asks about monthly cost, loan breakdown, or total interest.”
- 工具没接线:节点躺在画布上,但没连到 AI Agent 的
ai_tool输入。检查工作流 JSON 的connections块是否有"<tool_name>": { "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]] }。 - 名字违反
[A-Za-z0-9_]+:v1.1+ 上空格、连字符、emoji 会导致工具被静默跳过。
- 名字太泛(
错误 5:LLM 向query传入垃圾内容
- 症状:
JSON.parse(query)抛错,或字段类型不对。 - 原因:非结构化模式 + 描述含糊,LLM 只好自己发明格式;或要求 JSON 字符串但 LLM 发来自然语言;或数字字段被 LLM 序列化成了字符串。
- 按优先级修复:
- 切到结构化模式:
specifyInputSchema: true并定义字段,LLM 拿到类型化 schema,n8n 在代码运行前完成校验。 - 在描述中给出具体示例——LLM 善于模仿示例。
- 防御性强制转换:
- 切到结构化模式:
const params = JSON.parse(query); const price = Number(params.price); if (!isFinite(price)) throw new Error('price must be numeric');错误 6:"$helpers is not defined"/"$input is not defined"
- 原因:假设 Code Tool 沙箱暴露了与 Code 节点相同的辅助函数。它没有。
- 不可用的 API:
$input、$json、$binary、$node["OtherNode"]、$helpers.httpRequest()、$jmespath()、this.getContext(...)、$getWorkflowStaticData(...)、$fromAI()。 - 修复:纯计算就留在 Code Tool 里用原生 JS;需要 HTTP 就迁移到HTTP Request Tool(在 URL/body 中可用
$fromAI());需要其他节点数据或凭据就迁移到Call Sub-workflow Tool(toolWorkflow)——它的子工作流拥有完整的 Code 节点沙箱;需要跨调用状态就用子工作流读写 Data Table、Redis 等。
错误 7(Python 专属):"name 'query' is not defined"
- 原因:Python 中输入变量是
_query(下划线前缀),不是query。
# ❌ 错误 result = process(query) # ✅ 正确 result = process(_query)调试技巧
- 看执行视图(Execution view)而不只看测试输出:Agent 的工具调用和原始输入/输出都在那里,你能看到 LLM 实际发送的
query。 - 在工具内打日志:把收到的输入回显进返回的 JSON,例如
return JSON.stringify({ received_query: query, result: /* ... */ });——LLM 能看到回显,你也能发现畸形输入。 - 绕过 LLM 单独测工具:临时把工具节点改成带硬编码
query的独立 Code 节点手动运行,测完再换回来。
七、沙箱里没有的东西
Code Tool 沙箱比 Code 节点沙箱更窄,不要假设辅助函数会延续:
| 辅助项 | Code 节点 | Code Tool |
|---|---|---|
$input.all()、$input.first()、$input.item | ✅ | ❌ |
$node["NodeName"] | ✅ | ❌ |
$json、$binary | ✅ | ❌ |
$fromAI() | ❌ | ❌(哪怕它就坐在 AI Agent 旁边) |
this.helpers.httpRequest() | ✅ | ❌ |
DateTime(Luxon) | ✅ | ✅(JS 沙箱标准能力) |
$jmespath() | ✅ | ❌ |
this.getContext(...) | ✅ | ❌ |
$getWorkflowStaticData(...) | ✅ | ❌ |
结论:Code Tool 只适合纯计算。如果需要 HTTP 调用、API 查询或跨调用状态,请换用其他工具节点。
八、何时用 Code Tool,何时用替代方案
| 你的需求 | 选它 |
|---|---|
| 纯确定性计算(数学、解析、格式化、校验) | Code Tool |
需要多参数 + 干净的$fromAI()类型绑定 | toolWorkflow(子工作流工具) |
| 本质就是一次 API 调用 | HTTP Request Tool |
需要访问this.helpers、凭据或其他节点 | toolWorkflow |
| 跨调用持久状态 | toolWorkflow+ Data Table / Redis |
| 逻辑要在多个 Agent 间复用 | toolWorkflow |
| 想把代码内联在工作流里,不想拆子工作流 | Code Tool |
| 想要结构化类型输入但不想手写 JSON Schema | toolWorkflow |
经验法则:一旦你发现自己想用$fromAI(),你大概率该用toolWorkflow而不是toolCode。
九、完整可运行示例:汽车贷款计算器
一个生产级计算器工具(非结构化、字符串内嵌 JSON 模式),可直接作为工作流 JSON 中的节点对象:
{ "parameters": { "name": "calculate_car_loan", "description": "Computes monthly car-loan payment using an annuity formula with residual/balloon. Call with a single JSON string. Example: {\"price\":439900,\"down_payment\":87980,\"interest_rate\":6.95,\"months\":36,\"residual_percent\":50,\"setup_fee\":695,\"monthly_admin_fee\":59}. Required: price, down_payment, interest_rate, months, residual_percent. Optional: setup_fee, monthly_admin_fee (default 0).", "language": "javaScript", "jsCode": "let params;\ntry {\n params = typeof query === 'string' ? JSON.parse(query) : query;\n} catch (e) {\n throw new Error('Invalid JSON: ' + e.message);\n}\n\nconst price = Number(params.price);\nconst down_payment = Number(params.down_payment);\nconst interest_rate = Number(params.interest_rate);\nconst months = Number(params.months);\nconst residual_percent= Number(params.residual_percent);\nconst setup_fee = Number(params.setup_fee ?? 0) || 0;\nconst monthly_admin_fee = Number(params.monthly_admin_fee ?? 0) || 0;\n\nif (!isFinite(price) || price <= 0) throw new Error('price must be > 0');\nif (down_payment < 0 || down_payment >= price) throw new Error('down_payment must be in [0, price)');\n\nconst principal = price - down_payment;\nconst residual = price * (residual_percent / 100);\nconst r = interest_rate / 100 / 12;\nconst growth = Math.pow(1 + r, months);\nconst base = r === 0\n ? (principal - residual) / months\n : (principal - residual / growth) * r / (1 - 1 / growth);\nconst monthly_payment = base + monthly_admin_fee;\n\nreturn JSON.stringify({\n monthly_payment_sek: Math.round(monthly_payment),\n loan_amount: Math.round(principal),\n residual_value_sek: Math.round(residual),\n total_cost_of_credit: Math.round(monthly_payment * months + residual + setup_fee - principal)\n});" }, "type": "@n8n/n8n-nodes-langchain.toolCode", "typeVersion": 1.3, "name": "calculate_car_loan" }通过ai_tool连接类型把它接进 AI Agent 即可。这个示例完整演示了本指南的全部要点:描述里给 JSON 示例(让 LLM 知道发什么)、防御性JSON.parse、逐字段Number()强转、面向 LLM 的throw错误信息、以及最终JSON.stringify返回结构化结果。
十、与其他技能的关系
- n8n-code-javascript:Code节点技能。大多数 JS 模式(数组、map/filter、DateTime)可以迁移,但 I/O 契约不同,别复制数据访问代码。
- n8n-node-configuration:
specifyInputSchema是典型的 displayOptions 驱动的条件字段,可用get_node({detail: "standard"})查看toolCode的 schema 相关属性。 - n8n-workflow-patterns:Code Tool 位于“AI Agent with tools”模式内,Agent 通常挂多个工具,Code Tool 是其中的“本地计算”选项。
- n8n-validation-expert:本指南列出的三个签名错误有清晰特征——校验若报
"Wrong output type returned",就知道该把条目数组换成字符串。仓库的 ai-tool-validators.ts 正是这类校验逻辑的实现。
十一、部署前自检清单
- 节点类型是
@n8n/n8n-nodes-langchain.toolCode(不是nodes-base.code) - 工具名描述性强、动词化、snake_case(如
calculate_car_loan) - 描述说明何时使用该工具,非结构化时给出 JSON 示例
- 输入从
query(JS)或_query(Python)读取 - 代码体中没有
$fromAI() - 没有
$input/$json/$helpers——它们不在沙箱里 - 返回的是字符串(结构化输出用
JSON.stringify()) - 通过
ai_tool连接接入 AI Agent - 用LLM 实际会发的输入类型测试过(字符串内嵌 JSON,或 schema 校验过的对象)
十二、深入阅读
- INPUT_SCHEMA.md——结构化输入(DynamicStructuredTool)完整配置:
fromJson与manual两种风格、版本兼容性、决策树。 - ERROR_PATTERNS.md——全部错误目录:精确报错字符串、根因、修复与调试技巧。
- README.md——技能总览、激活条件与成功度量。
- ai-tool-validators.ts——仓库内 Code Tool 校验实现,可作为自动化验证时的权威参考。
最后再强调一次:Code Tool 是套着 Code 节点外衣的 LangChain 工具,契约是字符串进、字符串出。其余所有规则都从这一条推导而来——记住它,你就不会再被那些看似神秘的报错卡住。
【免费下载链接】n8n-mcpA MCP for Claude Desktop / Claude Code / Windsurf / Cursor to build n8n workflows for you项目地址: https://gitcode.com/GitHub_Trending/n8/n8n-mcp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考