n8n 生产级聊天机器人实战:Chat Agent 的 Shell + Core + Sub-Agents 多工作流组合模式(n8n-mcp)
【免费下载链接】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 Agents Skill(data/skills/n8n-agents/)中CHAT_AGENT_PATTERNS.md的深度展开,核心讲解面向 Slack、Discord、Microsoft Teams、Telegram 及嵌入式 Webhook 聊天等外部聊天入口的多工作流组合架构:防循环过滤、Shell 外壳、Agent Core 大脑、Sub-Agent 专家四层如何拆分与协同,以及各聊天平台的专属陷阱。读完本文,你将掌握一套可直接落地的"防死循环 + 加载态 UX + 线程会话 + 无状态子代理"生产级聊天机器人设计方案,并能借助 n8n-mcp 提供的 MCP 工具(n8n_update_partial_workflow、n8n_get_workflow、validate_workflow、n8n_test_workflow)逐节点构建与验证这些工作流。
第一条铁律:防循环过滤(Anti-Loop Filtering)
任何由聊天触发器发起、且会回复消息的工作流,必须在触发器之后立即过滤掉机器人自己的用户 ID,否则它会无限触发自身——每一次回复都会触发新的一次运行,直到触发速率限制或 n8n 并发保护,甚至可能拖垮整个 n8n 实例。这是每一个机器人(无论简单还是复杂)的最低底线,也是 n8n-agents 技能体系中反复强调的"聊天机器人无限循环"问题的标准解法。
优先在触发器层面过滤
当触发器原生支持过滤时,优先使用触发器级过滤——循环在任何一个下游节点运行之前就被切断。但各平台的语义差异很大,务必对照你的 n8n 版本逐一核实:
| 平台 | 触发器类型 | 过滤语义 |
|---|---|---|
| Slack | n8n-nodes-base.slackTrigger | options.userIds是排除列表(黑名单)——列出的用户在工作流运行前被丢弃。把机器人自己的 User ID 放进去。(已在触发器源码中验证:源码在if (userIds.includes(event.user))时提前 return。) |
| Telegram | n8n-nodes-base.telegramTrigger | additionalFields.userIds是包含列表(白名单)——只有列出的用户才能触发。它不是机器人排除过滤器;且 Telegram 机器人默认看不到自己的消息,因此通常不需要防循环。用白名单把私有机器人限制到特定真人即可。 |
| Discord、Teams | 无 | 没有原生的用户级触发器过滤——必须使用下游的 Filter 节点。 |
Slack 触发器级过滤示例
{ "parameters": { "trigger": ["message"], "channelId": { "__rl": true, "mode": "list", "value": "<CHANNEL_ID>" }, "options": { "userIds": "={{ [\"<BOT_USER_ID>\"] }}" } }, "type": "n8n-nodes-base.slackTrigger" }触发器不支持时的兜底:首个节点即过滤
当触发器不提供可用的排除过滤器时,触发器后的第一个节点必须丢弃机器人自己的 ID:
{ "parameters": { "conditions": { "conditions": [ { "leftValue": "={{ $json.user }}", "rightValue": "<BOT_USER_ID>", "operator": { "type": "string", "operation": "notEquals" } } ] } }, "type": "n8n-nodes-base.filter" }机器人用户 ID 来自你机器人的认证信息中的 API ID(Slack 的bot_user_id、Discord 的 application ID、Teams 的botId)。
何时拆分为 Shell + Core + Sub-Agents
在防循环过滤之外,一个简单机器人(一个触发器 → 一个 Agent → 一条回复,外加过滤器)完全可以放在单个工作流里。Shell + Core + Sub-Agents 的拆分是为生产级健壮性服务的,当以下任一条件成立时才值得拆:
- 机器人需要加载中状态的 UX(打字指示器、reaction、占位消息)以及超越单条消息的优雅错误处理;
- 机器人被多个聊天入口同时调用(比如 Slack 和 Discord 都要);
- 存在专业领域(Notion 数据库 schema、CRM 自定义字段、Linear 标签),Agent 不该把这些内联携带;
- Agent 或其工具会被跨工作流复用。
如果以上都不成立,保持单个工作流(过滤器仍然必须保留)。拆分后的形态如下:
[chat-surface workflow] ──► [agent core workflow] ──► [sub-agent workflows] ("the shell") ("the brain") ("specialists") - 从聊天入口触发 - 无状态 - 每个只负责一个窄领域 - 防循环过滤 - chatInput + threadId - 仅 chatInput - 路由 / 事件类型 - 基于 threadId 的内存 - 各自的工具 + 模型 - 加载态 + 错误 UX - 工具、子代理 - 渲染回复 - 不关心聊天入口细节完整的三个节点对象示例(无状态 Agent Core、Slack 路由 Shell、Notion 领域 Sub-Agent)见 EXAMPLES.md——它们是社区版 n8n JSON 片段,用于适配而非直接导入,凭证 ID、工作流 ID、频道/机器人 ID 均为占位符,需用n8n_update_partial_workflow(在ai_*输出上执行addNode+addConnection)构建,再用n8n_get_workflow和validate_workflow验证。
Shell:聊天外壳层
Shell 接收聊天事件,决定是否回应,管理 UX,调用 Core,渲染回复。它不做推理、不用 LLM,全是确定性逻辑。
按事件类型分流
同一个触发器会同时触发消息、reaction、提及、斜杠命令、按钮点击等不同事件。在防循环过滤器之后放一个Switch,把每种事件路由到正确的处理器:
"owner message" → Execute Workflow: agent-core "owner reaction" → no-op(或一个 reaction 处理器) "unknown user" → 固定文案回复 "slash command: /summary" → Execute Workflow: summary-command "button click" → Execute Workflow: interaction-handler每个 case 都是独立的子工作流,因为路由决策和实际工作是不同的关注点(不同的模型、超时、内存形态)。新增一个斜杠命令 = 一个 Switch 输出 + 一个子工作流,而不是新的顶层触发器。
Slack 专属注意(payload 形态随版本演进,硬编码路径前务必用一个真实事件验证):reactions / mentions 通过 Slack Trigger 以 Events API 事件流入;但斜杠命令和 Block Kit 按钮点击通常不会(Slack 把这些投递到单独的 Request URL)。它们需要通过第二个 Webhook 节点喂给同一个 Switch,或使用社区 Socket Mode 节点。斜杠命令暴露command字段;Block Kit 交互以type === 'block_actions'和actions数组到达。
加载状态 UX:每个退出路径都要清理
用户在没有确认信号时会认为什么都没发生。模式:在 Agent 调用前添加加载指示器,并在所有退出路径上移除它——包括错误路径。
[Trigger] → [Filter bot] → [Switch] → (owner message) → [Add loading reaction] (:spinner:, 等) → [Execute Workflow: Agent core] onError: 'continueErrorOutput' ├── (success) → [Remove reaction] → [Send reply] └── (error) → [Remove reaction] → [Send error message with link]错误路径是最容易遗漏的——没有它,指示器会永远挂在那里,用户会以为机器人还在工作。Execute Workflow 节点上的onError: 'continueErrorOutput'启用第二个分支(详见 n8n-error-handling)。对 Discord / Telegram,打字指示器是有时间上限的;对耗时较长的 Agent,先发送一条占位消息再编辑它。
线程即会话连续性
用聊天入口的线程原语作为记忆的sessionKey:
"workflowInputs": { "value": { "chatInput": "={{ $('Filter bot').item.json.text }}", "threadId": "={{ $('Filter bot').item.json.thread_ts || $('Filter bot').item.json.ts }}" } }thread_ts || ts是 Slack 的经典惯用法:线程内的回复携带thread_ts(指向父消息),而父消息只有ts。回退到ts让父消息成为其线程的会话键,于是每个线程都是一段全新对话,记忆不会跨线程泄漏。**只用用户 ID、频道 ID 或工作区 ID 是错的——它们会串会话。**发送回复时也要指向同一线程(otherOptions.thread_ts.replyValues.thread_ts= 同一个thread_ts || ts)。
错误 UX:暴露出来,不要挂死
错误分支发送一条带失败执行链接的简短消息:
There was a workflow error. https://<n8n-host>/workflow/<id>/executions/{{ $execution.id }}$execution.id是错误触发时刻的实时执行 ID。主机地址要跨环境参数化。
Agent Core:大脑层
Agent Core 是一个子工作流,声明两个输入:chatInput(用户消息)和threadId(聊天入口的线程/会话 ID)。它返回 Agent 的最终输出——字符串、结构化对象、或聊天入口专属的信封(Block Kit、adaptive card)。
除 MEMORY.md 之外,唯一聊天专属的接线就是把threadId直接接到sessionKey:
"sessionIdType": "customKey", "sessionKey": "={{ $json.threadId }}"threadId的流动路径是:触发器 →(透传节点)→ 记忆。绝不能把它放在$fromAI后面——Agent 会伪造一个 UUID(这是 SUBWORKFLOW_AS_TOOL.md 反复强调的"plumbed 参数"原则:身份、会话、限额这类确定性值必须由工作流侧注入,Agent 不可见、不可改)。
按执行次变化的上下文(用户身份、附加文件)放在 Agent 之前的 Set 节点里,并模板化进系统提示词(见 SYSTEM_PROMPT.md 的 "file-handling injection" 与 "piecing" 两节)。不要投机性地加 Set 节点——在复用成为现实之前,直接内联在systemMessage里就够了。
Block Kit / adaptive cards:必须搭配 outputParserStructured
Block Kit / adaptive cards 必须与outputParserStructured配对(详见 STRUCTURED_OUTPUT.md)。"用schemaType: 'manual'+ 真实 JSON Schema"的指导在这里更加严格:Block Kit 和 adaptive card 依赖跨 block 类型的oneOf联合类型,加上每个 block 内的枚举(style等)——jsonSchemaExample完全无法表达这些,而且会产生"自信地错误"的 block 树,最终被聊天入口拒收。EXAMPLES.md 的 Agent Core 片段给出了一个真实可用的 manual schema 示例(header/section/divider三种 block 的oneOf联合),并配autoFix: true和独立的 coding-capable 修复模型。
Block Kit 信封陷阱(Slack 专属)
当 Agent 返回 Block Kit、你通过 Slack 节点的blocksUi发布时,值必须是形如{ "blocks": [...] }的对象,其中值必须是真实数组——既不是裸数组,也不是字符串化的数组:
✅ ={{ { "blocks": $('Call Agent core').item.json.output.blocks } }} ❌ ={{ $('Call Agent core').item.json.output.blocks }}只传数组会静默失败——Slack 节点接受了输入,消息发布出来却没有富内容,且没有任何错误或警告。详见 NODE_FAMILY_GOTCHAS.md 的 Slack 章节。
Sub-Agents:把 Agent 当作工具
Sub-Agent 是自己的工作流 + 自己的 Agent 节点,通过.toolWorkflow从路由 Agent 调用。以下情况值得引入:
- 领域有一组路由 Agent 不该携带的 schema / 枚举(Notion DB 属性、Linear 标签、CRM 字段);
- 领域有 5+ 个工具,会塞满路由器的工具列表;
- 该能力被多个路由器复用;
- 该领域值得用比路由器更便宜/更快的模型。
契约:无状态(Stateless)
**契约是无状态的。路由器在chatInput中发送完整请求——没有共享记忆,没有隐式上下文。必须在工具描述(路由器侧)**和 **Sub-Agent 的系统提示词(被调用侧)**两侧同时强化:
IMPORTANT: This tool is stateless. Send all relevant context in a single message. If you need to create an entry, include ALL required fields upfront.
没有这条,路由器会假设隐式上下文存在,Sub-Agent 则靠猜。其余关于子工作流即工具的接线细节(类型化输入、$fromAI映射、plumbed 参数、输出形状契约、Stop and Error与onError: 'continueErrorOutput'的取舍、独立测试)→ SUBWORKFLOW_AS_TOOL.md。
新鲜 Schema 注入
当领域 schema 可能运行时变化时(Notion DB 选项会演进、Linear 团队会加标签),每次 Sub-Agent 调用都重新拉取,而不是硬编码:
[Execute Workflow Trigger] ↓ [Notion: Get Database] # 拉取实时 schema ↓ [Agent] system prompt template 包含: ## Database Schema {{ $('Get a database').first().json.properties.toJsonString() }}代价是每次调用多一次 API 请求;换来的好处是 Sub-Agent 永远不会因为提示词过期而返回 "that property doesn't exist"。对低并发的聊天助手很划算;对高并发热点路径,把 schema 缓存到 Data Table 并设置 TTL。
EXAMPLES.md 的第三个片段(Notion ideas sub-agent)正是这个模式的完整实现:Get a database在main上先于 Agent 运行,properties通过.toJsonString()模板化进系统提示词,Sub-Agent 跑在比路由器更便宜的模型上(示例为claude-haiku-4.6),maxIterations也相应降到 15(宽路由器为 50)。
反模式速查表
| 反模式 | 出错表现 | 修复 |
|---|---|---|
| Shell 顶部没有机器人用户 ID 过滤 | 机器人自己的消息重新触发工作流——无限循环 | 触发器级排除(Slackoptions.userIds),或首个节点 Filter$json.user !== '<BOT_USER_ID>' |
把机器人 ID 放进 Telegram 的userIds期望排除 | 它是白名单——只有机器人会触发,真人全部被挡;看起来"修好了"实际是静默失败 | Telegram 机器人默认看不到自己的消息;userIds只用于给真人放行 |
| 只在成功路径移除加载指示器 | 任何错误后用户都看到机器人永远"思考中" | onError: 'continueErrorOutput'+ 两个分支都移除 |
| 用用户/频道/工作区 ID 做会话键 | 同一频道内不同线程的对话互相串扰 | 使用线程原语(Slackthread_ts || ts) |
| 已需要多入口/子代理/复用时仍用单工作流 | 无法复用、UX 泄漏进推理逻辑、难以隔离测试 | 拆成 Shell + Core + Sub-Agents(仅在需求真实存在时) |
| Sub-Agent 读写共享记忆 | 调用方无法推理其行为、无法安全重试 | Sub-Agent 无状态——完整上下文放chatInput |
| Sub-Agent 提示词中硬编码领域 schema | Schema 腐烂,Sub-Agent 之后选到无效选项 | 运行时重新拉取并模板化 |
把裸 blocks 数组传给blocksUi | Slack 发布空消息,无任何错误 | 包装为{ "blocks": [...] }真实数组 |
与 n8n-mcp 的工程落地衔接
这套模式在本仓库中不仅是一份设计文档,还与 MCP 服务器暴露的构建工具链直接对应,形成"设计 → 构建 → 验证"的闭环:
- 构建:EXAMPLES.md 中的三个节点对象片段,按文档说明通过
n8n_update_partial_workflow的addNode+addConnection(在ai_*输出上)组装,而不是一次性整包导入。MCP 工具的分发与 schema 定义见 src/mcp/server.ts 与 src/mcp/handlers-n8n-manager.ts。 - 验证:组装后使用
n8n_get_workflow(mode: 'structure')核对结构,再用validate_workflow做校验;n8n_test_workflow工具支持对子工作流独立测试(schema 位于 src/mcp/handlers-n8n-manager.ts),与 SUBWORKFLOW_AS_TOOL.md 中"用钉住数据独立测试工具子工作流"的建议一致。 - 会话模型:仓库的 Chat Trigger 处理器(src/triggers/handlers/chat-handler.ts)展示了与本文
threadId一致的会话思维——它通过crypto.randomUUID(CSPRNG,122 位熵)生成不可猜测的sessionId,并以{ action: 'sendMessage', sessionId, chatInput }的 payload 结构发送到聊天 Webhook(src/triggers/handlers/chat-handler.ts、src/triggers/handlers/chat-handler.ts)。这印证了本文的核心原则:会话键必须稳定、确定、由工作流侧生成并贯穿记忆与工具,绝不可交给 Agent 决定。
关键参考与延伸阅读
本主题在技能体系中的完整上下文:
- 本文档的母文档与技能总览:data/skills/n8n-agents/SKILL.md
- 三个完整节点对象示例(无状态 Agent Core / Slack 路由 Shell / Notion 领域 Sub-Agent):EXAMPLES.md
- 工具命名、描述、
$fromAI的完整规则:TOOLS.md .toolWorkflow的形状与参数映射:SUBWORKFLOW_AS_TOOL.md- 按执行次变化的上下文、文件注入、提示词存储:SYSTEM_PROMPT.md
- 解析器配置、autoFix、修复模型:STRUCTURED_OUTPUT.md
- 记忆类型、
sessionKey持久化:MEMORY.md onError: 'continueErrorOutput'与错误 UX:data/skills/n8n-error-handling/- Slack 节点参数形态(Block Kit):NODE_FAMILY_GOTCHAS.md(Slack 章节)
- 各入口接收上传文件 / 返回生成文件的机制:data/skills/n8n-binary-and-data/
- 高层"工作流中的 Agent"骨架:ai_agent_workflow.md
最后一条总结:模型看不到你的接线——它看到的是一条系统提示词和一组有名字、有描述的工具。把防循环过滤当作触发器的第一道闸,把threadId当作会话的唯一钥匙,把每个 Sub-Agent 当作一个无状态的 API,大多数"机器人行为异常"的问题在落地前就会消失。
【免费下载链接】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),仅供参考