技术

介绍一下增强聊天机器人代理的 Workflow

AAnonymous
12分钟阅读

开头

这篇文章是 Omnilude 的 ai-service 为什么采用 Assistant / Thread / Run 结构Omnilude 的 ai-service 中 Agent 是如何运行的 的后续篇。

前两篇主要讲结构。这一次,我想直接看这套结构在后台工作画布里是怎样真正运转的。说明对象就是 增强聊天机器人

这篇文章不是重新捏造一个抽象示例,而是基于真实写好的 workflow 配置,按原样说明这个代理接收什么输入、在哪里分支、经过哪些 assistant 和 tool,最后怎样生成答案。

用一句话说,增强聊天机器人是什么

这个代理本质上是一个把问题分成四条路径处理的分支型聊天机器人,而且这种分支是非常明确的。

  • 普通问题就直接回答。
  • 需要最新信息时走搜索路径。
  • 输入是 YouTube URL 时走字幕摘要路径。
  • 输入是普通网页 URL 时走文章分析路径。

把真实 workflow 简化之后,大概是这样。

也就是说,这个 workflow 的核心不是蛮力地把一个模型调用到极致,而是先判断问题属于哪一类,再为它选择对应的处理管线。

我其实很想把它公开出来让大家直接体验,但 LLM 调用本身有成本,所以目前还没有对外开放。等条件更宽裕一些时,我会考虑以可实际运行的形式开放。

起点就是一个文本输入

这个代理的起始节点是 text-input,示例值写的是 最近的社会议题是什么?。这当然只是一个演示用语句,但它也很清楚地说明了这个 workflow 想解决的问题类型。

拿到一个问题后,它不会立刻扔给 LLM,而是先送到 router 节点。这里使用的 assistant 是 Question Router,实际配置的路由源一共有四个。

  • llm_direct: 直接回答
  • web_search: 网页搜索后再回答
  • youtube_summary: YouTube 摘要
  • article_analyze: 网页分析

这个 router 不是简单的 if 语句。assistant 的 instructions 里明确写了在什么情况下该选什么 source,而且响应被强制要求以 JSON 返回。比如需要最新信息、基于日期的信息、或者需要事实核查的问题,会被送到 web_search;YouTube URL 去 youtube_summary;普通 URL 去 article_analyze;其他情况则去 llm_direct

同时 router 不只会留下 source,还会留下 reason。画布上挂着的一个 text-visualize 节点,就是专门用来肉眼查看这个理由的调试查看器。也就是说,这个页面不是单纯的设计图,而是可以直接查看执行痕迹的工作界面。

直答路径是最短的

一旦分支落到 llm_direct,结构就很简单。router 的输出进入使用 Question Answer assistant 的 generate-text 节点,而这个节点的输出会直接流向 finish

这里重要的一点是,路径简单并不代表结构也随意。generate-text 节点会在执行时通过 aiAssistantId 重新加载 assistant,把 system prompt 和 user prompt 组合起来,然后才发起真正的模型调用。也就是说,即使画布上附带了 assistant 的元数据,真正的运行时依据依旧是 assistant id。

增强聊天机器人里挂接的 assistant 如下。

Router: Question Router (assistantId=1)
Direct answer: Question Answer (assistantId=2)
Search query writer: Web Search Expert (assistantId=12)
Search-based answer: AdaptiveAnswerer (assistantId=3)
YouTube writer: YoutubeSummarizer (assistantId=15)
Article writer: ArticleAnalizer (assistantId=16)

正因为如此,同一个 generate-text 节点,只要接上不同的 assistant,就会承担完全不同的角色。节点本身是通用执行器,角色由 assistant 决定。

搜索路径不是两步,而是四步

这个代理真正的重点在 web_search 路径。看现在的实现可以发现,基于搜索的回答并不是一次性生成的,而是被明确拆成了几个阶段。

第一阶段是 Web Search Expert。这个 assistant 的职责是把用户问题改写成一条可以直接扔给搜索引擎的查询语句,而不是拿原始问题直接搜索。

第二阶段是 web-search-tool。在保存下来的 workflow 配置里,这个节点被设置为 searchTool: "searx",而实际节点实现会调用 SearXng,最多收集 10 条搜索结果摘要,并设置 200 秒超时,避免无限等待。

第三阶段是 prompt-crafter。这个节点不会把搜索结果直接贴进最终回答,而是把 {{results}} 注入预先保存的模板里,生成一个 回答用 system prompt。这个模板里已经包含了当前时间、参考资料、回答规则以及写作语气。

第四阶段是 AdaptiveAnswerer。这里最有意思的点在于,原始用户问题仍然作为 prompt 保留,而通过搜索整理出来的上下文则进入 system。也就是说,搜索结果不是被机械地拼接上去,而是在问题与证据分离的前提下,再由负责写作的 assistant 重新组织成答案。

把这个流程再画一遍,就是这样。

我觉得这部分很重要。它没有因为接了搜索就马上做成一个看起来像 RAG 的黑盒,而是把问题整理、证据拼装、最终写作拆开了。这样在实际运营时,也更容易看出是哪一环在抖动。

YouTube 与 Article 路径是 tool-first

URL 相关的分支更直观一些。

如果输入是 YouTube URL,先执行的是 youtube-summary-tool。这个节点会通过 YoutubeTranscriptTool 获取带时间信息的 transcript,然后交给 YoutubeSummarizer assistant,把它整理成更适合阅读的内容。

如果输入是普通网页 URL,先执行的是 article-analyzer-tool。这个节点会用 Crawl4Ai 抓取页面,并在可能的情况下用 Readability4J 只提取正文,再整理成带标题的 markdown。之后再由 ArticleAnalizer assistant 把它重组为更易读的文章。

也就是说,这两条 URL 路径遵循的是同一套思路。

  • 先由 tool 取回原材料。
  • 再由 assistant 把原材料整理成人可以阅读的结果。

这一点和搜索路径也很像。在 Omnilude 当前的 agent 设计里,tool 和 assistant 并不是竞争关系。tool 负责取材,assistant 负责理解和整理。

这个画面并不只是好看的图

你看后台画布时,会发现中间穿插着一些 text-visualize 节点。这些节点不是生成最终结果的必需品,但它们能让你立刻看到当前到底有什么值在流动。

前端实现也是按这个方向设计的。工作流执行时,如果某个节点收到 NODE_COMPLETED 事件,前端就会把这个节点的输出值注入到画布状态里。text-visualize 节点会直接显示这个值,而如果有流式 delta 到来,它还会持续累积文本。

这个差别比看上去更重要。很多 workflow 工具停留在“把节点画出来”这一步。但 Omnilude 当前后台的 agent 页面,把“查看保存下来的图”和“观察真实执行中的中间值”这两件事合在了一起。所以这块画布既是设计图,也是调试器。

执行引擎是这样理解它的

再往画布外走一步。这个 workflow 在数据库里是以 ai.ai_agent.workflow JSONB 的形式保存的。它不是拆成独立节点表,而是整体以一份图结构 JSON 持有。

执行开始时,BasicWorkflowEngine 会先去找 type-input 结尾且 startNode=true 的节点。这里的起点就是 text-input。从那之后,NodeExecutorFactory 会根据节点类型挑选对应的 executor,把执行继续往下推进。

对于 增强聊天机器人,关键 executor 大致可以概括为以下这些。

  • TextInputNode: 准备起始输入
  • RouterNode: 使用 assistant 选择分支
  • GenerateTextNode: 基于 assistant 生成文本
  • WebSearchToolNode: 执行 SearXng 搜索
  • YoutubeSummaryToolNode: 提取 YouTube transcript
  • ArticleAnalyzerToolNode: 清洗网页正文
  • PromptCrafterNode: 生成基于模板的 system prompt
  • FinishNode: 汇总各分支结果并结束执行

这里最关键的一点是,generate-textrouter 都会在执行时重新加载 assistant。也就是说,agent 本身并不直接拥有智能,而是各个节点在需要时再把 assistant 拉进来使用。归根结底,agent 是 orchestration,真正的推理依然是 assistant 在做。

finish 节点也没有看起来那么简单

最后的 finish 节点并不只是一个“结束”按钮。它会检查进入自己的 edge,如果还有仍在进行中的输入 handle,就先等待;等准备好的源节点输出出现后,再把它们收集为最终结果。

之所以需要这种结构,是因为这里存在分支。router 分成四条路径,并不意味着这四条路径总会以同样方式完成。有的问题直接走直答结束,有的问题则要继续经过搜索和摘要。finish 就是吸收这些差异的最后收集器。

所以,这个代理并不是“输入一个问题,输出一个答案”的函数,而是运行在带有分支、等待与合流能力的图执行器之上。

这个代理想表达什么

我觉得,这个看似简单的代理其实很能代表 Omnilude 当前使用 AI 的方向。

第一,它没有夸大所谓通用 agent。它先做问题类型划分,再把合适的 tool 和 writer 以非常务实的方式接上去。

第二,它没有把搜索路径压成一个黑盒。问题整理、搜索、上下文拼装、最终回答生成都被分开了。

第三,后台画布不是单纯的编辑器。它能看到执行中的中间值,让 prompt 实验和调试发生在同一块屏幕上。

第四,assistant 和 agent 的职责非常清楚。assistant 是推理部件,agent 是连接这些部件的 workflow。

从这个角度看,我觉得这次实现与其说是“做了一个 AI agent”,不如说是“把问题处理管线做成了可视化、可运维的形式”。

结尾

如果前两篇文章解释的是 assistant、thread、run、agent 的结构,那么这一篇就是在展示这些解释落实到真实画布和真实节点连接之后会是什么样子。增强聊天机器人通过一个 router、几个 writer assistant,以及搜索、YouTube、文章 tool 的组合,已经能作为一个相当实用的聊天机器人运行。

等条件更充足一些时,我也希望能基于图片和真实运行画面,用更容易理解的方式再讲一遍这个代理的构成与实现。