跳到主要内容

流式、人工审批、引用与去重

本章讲什么: 前三章把「agent 怎么思考和用工具」讲完了。本章讲面向的工程层:agent 怎么实时把进度推给前端、怎么在执行危险工具前停下来等你点同意、引用来源怎么挂回消息、以及怎么防止弱模型把同一个工具调到天荒地老。


1. websocket 插件:内核与前端的桥

agent 跑在 WebSocket 上(见 index 的全景图)。websocket 插件(server/utils/agents/aibitat/plugins/websocket.js)在 setup 时给 aibitat 实例挂上几样东西:

挂到 aibitat 上的东西作用
aibitat.socket.send(type, content)内核各处发事件的统一出口(websocket.js:84)
aibitat.introspect(text)推「思考中/状态」消息给前端(websocket.js:71)
aibitat.requestToolApproval({...})工具执行前请求人工审批(websocket.js:101)
onError 处理器出错时推 wssFailure 并 terminate(websocket.js:58)

内核在工具循环里到处调这些。比如第 02 章的 handleAsyncExecution 通过 eventHandler → socket.sendtextResponseChunk(token 流)、toolCallInvocation(正在拼工具调用)、usageMetrics(用量)等事件(index.js:988:1067)。前端按 type 字段渲染不同 UI。


2. 人工审批:执行危险工具前停一下

这是「human-in-the-loop」最实在的体现。某些工具(写文件、发邮件、删数据)不该让 agent 静默执行。requestToolApproval(websocket.js:101)在工具真正跑之前阻塞等待用户点「同意/拒绝」:

requestToolApproval({ skillName, payload, description })

├─ AGENT_AUTO_APPROVED_SKILLS 里? ─是─► 自动放行
├─ 该用户把它加白名单了? ─是─► 自动放行
└─ 否则:
socket.send("toolApprovalRequest", {requestId, skillName, ...})
return new Promise(resolve => {
socket.handleToolApproval = (msg) => { // 等前端回 toolApprovalResponse
if 匹配 requestId → resolve({approved: bool})
}
setTimeout(2 分钟超时 → 默认拒绝)
})

(websocket.js:101-189)。三层放行逻辑:AGENT_AUTO_APPROVED_SKILLS 环境变量(websocket.js:106 skillIsAutoApproved)→ 用户级白名单(AgentSkillWhitelist.isWhitelisted,websocket.js:121)→ 否则真的弹窗等人,2 分钟超时默认拒绝(TOOL_APPROVAL_TIMEOUT_MS,websocket.js:7)。

前端的回复怎么找回这个 Promise?WebSocket 端点的 relayToSocket(server/endpoints/agentWebsocket.js:12)把进来的消息按当前挂着的处理器中继——handleToolApproval / handleFeedback / handleClarificationResponse 三选一,或当成退出命令。这套「挂一个临时处理器、resolve 一个 Promise」的模式同时支撑了「审批」「问用户」「继续对话」三种人机交互。


3. 引用与附件:缓冲 + 收尾回灌

agent 用 RAG/文档工具时会产生引用来源(citations)。但工具是在循环中途执行的,而引用要挂到最终那条回复消息上(才知道它的 UUID)。解法是「缓冲—收尾刷新」:

  • 工具执行中,引用先进 _pendingCitations 缓冲区(index.js:50,addCitation/addDocumentCitations)。
  • 循环出口时,flushCitations(uuid) 把缓冲的引用一次性按最终消息 UUID 推给前端(index.js:233,在 handleExecution/handleAsyncExecution 的返回前调用)。
  • chat-history 插件持久化后再 clearCitations

工具产出的图片附件走类似路子:addToolAttachment_toolAttachments 缓冲(index.js:275),collectToolAttachments 在拼下一轮消息时取出、作为多模态 user 消息注入(index.js:1082),让模型看见图。


4. Deduplicator:阻止弱模型把工具调爆

第 02 章提过它,这里讲透——这是给弱模型擦屁股的关键防线(server/utils/agents/aibitat/utils/dedupe.js)。它有三种独立的拦截机制:

机制拦什么怎么判API
精确去重(hashes)完全相同的工具+参数重复调用sha256({key, params}) 命中过trackRun / isDuplicate
冷却(cooldowns)近期同名工具(不看参数)同名在冷却窗口(默认 30s)内startCooldown / isOnCooldown
唯一(uniques)一会话只能调一次的工具,如生成图表markUnique 标记过markUnique / isMarkedUnique
// isDuplicate 一次过三道闸(dedupe.js:60),命中任一即拦截
isDuplicate(key, params = {}) {
const newSig = sha256({ key, params });
if (this.#hashes[newSig]) return { isDuplicate: true, reason: "精确重复" };
if (this.isOnCooldown(key)) return { isDuplicate: true, reason: "冷却中" };
if (this.isMarkedUnique(key)) return { isDuplicate: true, reason: "标记为唯一" };
return { isDuplicate: false, reason: "" };
}

UnTooled 在每次工具调用前查 isDuplicate,命中就丢弃并提示模型(untooled.js:170:223);执行后 trackRun 记指纹(untooled.js:264)。MCP 工具会被自动加冷却——因为有些 MCP 工具无返回值,弱模型容易反复空调它(untooled.jsisMCPTool);可用 MCP_NO_COOLDOWN 关掉。一次会话结束时 reset("runs") 清空精确去重表,这样新对话里允许再用之前用过的工具(untooled.js stream/complete 末尾)。


5. 巧妙之处

  • 一个 Promise + 临时处理器,统一三种人机交互:审批、问用户、继续对话都用「socket.handleXxx 挂处理器 → 等前端回 → resolve」同一套(websocket.js:140endpoints/agentWebsocket.js:12)。
  • 引用「缓冲—按最终 UUID 回灌」:解决「引用产生于中途、却要挂到最终消息」的时序错位(index.js:233)。
  • 三机制去重:精确(SHA)防一字不差的重复、冷却(同名+时间窗)防高频空调、唯一(标记)防一次性工具被多调——分层拦不同的「调爆」模式(dedupe.js)。

6. 边界与局限

  • 审批超时(2 分钟)默认拒绝而非通过(websocket.js:182);整个 socket 5 分钟无活动超时(SOCKET_TIMEOUT_MS,websocket.js:6)。
  • 去重器是会话内状态,跨会话重置;它对「故意的重复操作」会误伤,需要 MCP_NO_COOLDOWN 或调整。
  • 引用/附件缓冲依赖循环正常走到出口刷新;若中途 abort,缓冲可能不落地。

7. 横向对比(同 shelf 兄弟)

AnythingLLM 的 agent 与本货架其他 chat-agent 的取舍差异:

  • 「弱模型也能当 agent」是它的独特卖点:UnTooled 用 prompt 模拟工具调用 + Deduplicator 防循环,专门服务本地/开源小模型;很多框架默认假设模型有原生 function-calling。
  • 能力来源最杂但收敛最统一:内置技能 + 无代码 Flow + MCP + Hub 插件四路,全归一到 aibitat.function()(见第 03 章)——可对照其他「只有内置工具」或「只有 MCP」的兄弟项目。
  • 人机交互内建在协议层:审批/问用户/继续是 WebSocket 一等公民,而非外挂。

(具体兄弟项目链接待货架总库 doc 补;此处给出对比维度。)


8. 代码地图

主题文件符号
websocket 插件server/utils/agents/aibitat/plugins/websocket.jswebsocket, requestToolApproval, introspect
消息中继server/endpoints/agentWebsocket.jsrelayToSocket, agentWebsocket
引用缓冲/刷新server/utils/agents/aibitat/index.jsaddCitation, addDocumentCitations, flushCitations
附件回灌server/utils/agents/aibitat/index.jsaddToolAttachment, collectToolAttachments
去重器server/utils/agents/aibitat/utils/dedupe.jsDeduplicator, isDuplicate, startCooldown, markUnique
弱模型集成点server/utils/agents/aibitat/providers/helpers/untooled.jsUnTooled, isMCPTool, streamingFunctionCall