让平台成为 Agent 的 LLM 节点:对话、工具调用循环、规划 Agent
30 秒导读: 前几章讲的是"一张图怎么被跑起来"(见 03-workflow-engine.md)。这一章讲图里那几个真正接大模型的节点:从"调一次模型就返回"的对话节点,到"模型自己决定调工具、看结果、再决定"的工具循环节点,再到"会先列计划、卡住了会反问用户"的规划 Agent。正是这几个节点,把 FastGPT 从"可视化工作流"升级成"Agent 平台"。
1. 这是什么(零基础也能懂)
一句话定义: 本章讲的是工作流里把大语言模型接进来的那几种节点——它们的区别不在"调哪个模型",而在"给模型多大的自主权"。
先建立最重要的一个直觉:自主权是逐级放开的。
| 节点 | 模型的自主权 | 一句话 |
|---|---|---|
| 基础对话节点 | 无。调一次,出答案 | 你问,它答 |
| 工具调用节点 | 中。可反复自己挑工具 | 你给它工具,它自己边想边用 |
| 规划 Agent 节点 | 高。会先列计划、卡住会反问 | 你给它目标,它自己拆步骤、追问、执行到完成 |
| 辅助 LLM 节点 | 极小。做一件固定小事 | 分类 / 抽字段 / 扩写问题 |
给谁用: 搭工作流的人。想做个"知识库问答机器人",用对话节点就够;想做个"能查资料、能算数、能调外部 API 的助手",用工具调用节点;想做个"给个复杂任务能自己规划着干完"的 Agent,用规划 Agent 节点。
一句话直觉/类比:
基础对话 = 一次性问答:模型是"应答机"
工具循环 = 模型手里多了一串遥控器(工具),它按需一个个按,看反馈再按下一个
规划 Agent = 工具循环 + 一块随身白板(plan):先在白板上写下步骤,
干一步勾一步,缺料就举手问你(ask),全部勾完才收工
本章不重复的部分: 节点怎么被调度器选中并喂进参数,已在 03-workflow-engine.md 讲过;知识库检索节点内部(向量化、混合检索)归 05-knowledge-base.md。本章只钻这几个节点自己的算法与多轮控制。
2. 顶层全景(它大概怎么转)
2.1 三层节点,一个共用内核
最关键的架构事实:工具循环节点和规划 Agent 节点,底下是同一个 while 循环 runAgentLoop。对话节点则不进这个循环,只调一次。
一次对话请求
│
┌─────────────┼──────────────────────┐
▼ ▼ ▼
① 基础对话节点 ② 工具调用节点 ③ 规划 Agent 节点
dispatchChat dispatchRunTools dispatchRunAgent
Completion │ │
│ ▼ ▼
│ runToolCall runUnifiedAgentLoop
│ (toolCall.ts) (unified.ts:叠加 plan/ask/stop-gate)
│ │ │
│ └───────────┬───────────┘
▼ ▼
createLLMResponse runAgentLoop ← 共用的多轮工具循环内核
(调一次模型) (loop/base.ts:while 里反复调模型 + 执行工具)
怎么读这张图:从左到右自主权递增。①走一条直线;②③都汇入 runAgentLoop 这个 while 循环,区别只在③在循环外面多包了一层 plan/ask/stop-gate 的编排(runUnifiedAgentLoop)。
2.2 各部件一句话职责
| 部件 | 干什么 | 在哪个文件 |
|---|---|---|
dispatchChatCompletion | 基础对话:组 prompt、拼知识库引用、流式返回 | dispatch/ai/chat.ts:59 |
dispatchRunTools | 工具节点入口:备料、算钱、拼 assistantResponses | dispatch/ai/toolcall/index.ts:21 |
runToolCall | 工具节点主编排:把职责拆成 hook,调 runAgentLoop | dispatch/ai/toolcall/toolCall.ts:34 |
runAgentLoop | 多轮工具循环内核:压缩→调模型→跑工具→判停 | ai/llm/agentLoop/loop/base.ts:186 |
dispatchRunAgent | 规划 Agent 入口:备工具/文件/沙箱/memory | dispatch/ai/agent/index.ts:113 |
runUnifiedAgentLoop | 单主 Agent 循环:在内核外叠 plan/ask/stop-gate | ai/llm/agentLoop/loop/unified.ts:134 |
| 辅助节点 | 分类 / 抽字段 / 问题扩写,各调一次模型 | classifyQuestion.ts、extract.ts、functions/queryExtension.ts |
3. 核心原理(逐个机制,由浅入深)
3.1 基础对话节点 dispatchChatCompletion
它要解决的小问题: 把"用户这句话 + 历史 + 知识库引用 + 系统提示"拼成一份合法的 messages,调一次模型,把答案流式吐回前端。
思路: 全程只有一次模型调用,没有循环。难点全在调用之前的 prompt 组装和调用期间的流式转发。
① 知识库引用怎么塞进去(user 还是 system?)。 getDatasetCiteData 决定引用文本放进 user 消息还是 system 消息:
- 若
aiChatQuoteRole === 'user',或引用模板里含{{question}}占位符 → 引用拼进 user 输入; - 否则 → 引用拼进 system 提示。
// 示意,非源码:引用角色的判定
const quoteRole =
aiChatQuoteRole === 'user' || datasetQuotePrompt.includes('{{question}}')
? 'user'
: 'system';
真实实现见 chat.ts:345 getDatasetCiteData 内 quoteRole 的计算;引用文本由 replaceVariable 把 {{quote}}/{{question}} 填进模板。
② 系统提示怎么拼。 getChatMessages 把三段用固定分隔符串起来——模型自带的默认系统提示、用户填的系统提示、知识库引用系统提示:
// 示意,非源码:三段系统提示拼接
const concatenateSystemPrompt = [
model.defaultSystemChatPrompt,
systemPrompt,
datasetCiteSystemPrompt
].filter(Boolean).join('\n\n===---===---===\n\n');
真实实现 chat.ts:410 concatenateSystemPrompt。随后 filterGPTMessageByMaxContext(chat.ts:469)按 maxContext - maxTokens 从旧到新裁历史,保证不超窗。
③ 流式转发。 调用 createLLMResponse 时挂了两个回调,把模型吐出的 reasoning / answer 增量实时推给前端 SSE:
onReasoning(chat.ts:213)→ 推reasoning_content;onStreaming(chat.ts:222)→ 推text。
两者都受开关控制(aiChatReasoning、isResponseAnswerText)。
④ 收尾。 算钱(formatModelChars2Points chat.ts:238,用户自带 key 则计 0 分),把 OpenAI 格式的 completeMessages 转回 FastGPT 的对话结构(GPTMessages2Chats chat.ts:254),作为 history 输出。
关键点: 对话节点没有"再问一次模型"的能力。它把
answerText同时挂到toolResponse(chat.ts:297),所以它也能被当作工具节点的一个子工具来调——但它自己内部不会循环。