Agent 怎么指挥干活、流式输出怎么

metal 1小时前 ⋅ 1 阅读
ad

 Agent 怎么指挥干活、流式输出怎么"流"、工作流怎么选

 

我折腾 AI 应用开发有一阵子了,中间走了不少弯路。这篇想把"Agent怎么工作,如何选择适合自己的工作流、以及怎么合理显示AI回复"这件事里,我最后悔没早点搞懂的三块东西讲清楚:Agent 到底是怎么识别 Skill、支配 Tool 干活的;流式输出是怎么流起来的;以及工作流有哪几种玩法,分别适合什么场景。

 

不敢说讲得多深,但保证说人话。

 

一、AI 的家当其实就三层:Agent、Skill、Tool

 

刚入行那会儿,我被这些词绕得头大。后来想了一个特别土的比喻,一下就通了:

 

Tool 是手,Skill 是老师傅的笔记,Agent 是学徒。

 

先看 Tool,它是底层真正干活的东西:查天气、读文件、搜网页、发请求,每个工具本质上就是一个函数。但它有个特点——自己不会主动干活,得有人喊它,而且得按它的规矩喊。每个 tool 都带一张"简历",写着三件事:我叫什么名字、我能干嘛、我收什么参数。大模型就是靠看这些简历,决定该喊谁、怎么喊。

 

然后是 Skill。它不干活,它就是一份操作手册,一个 markdown 文档而已。比如"调试页面"这本手册里写着:第一步,用 new_page 工具打开页面;第二步,调 list_console_messages 工具看控制台报错;第三步,遇到 my 开头的组件报错就去查 myuiList.md……从头到尾,skill 没有亲自做任何事,它只是把"遇到什么情况、该用什么工具、按什么顺序"这套老师傅的经验,写成了一本小册子。

 

最后是 Agent,它才是动脑子的那个。拿到你的话之后,它先扫一眼所有 skill 的简历(专业说法是"加载 skill 头部信息"),判断这活儿需要哪本手册;觉得需要,就调用一个叫 skill 的工具,把手册全文读进来;然后照着手册一步一步支配 tool 干活;干完再把结果翻译成大白话回给你。

 

所以那句被传得神乎其神的话——"agent 通过识别不同 skill,支配 tool 干活"——说白了就是:学徒翻手册、使工具,一点都不玄。

 

讲个真实的例子。最近我做的实战项目里,用户说"帮我调试下这个项目"。agent 翻了翻技能目录,发现有个叫 test-chrome 的技能,简介写着"当用户需要你调试某个项目的时候,阅读此 skill"。它就把这本手册读进来,按步骤先调 new_page 打开页面,再调 list_console_messages 拿控制台信息,发现报错再按手册去查对应文档。整个过程用户啥都没教,它自己把流程走完了。

 

这就是 skill 的价值:把"怎么干"沉淀下来,让 agent 不用每次都重新摸索。

 

技术上怎么落地?也不复杂:程序把一堆 tool 的 JSON 描述(名字、说明、参数格式)当参数传给大模型接口;模型回复里如果带了 tool_calls,程序就真的去执行这些函数,把结果塞回对话。一来一回,模型自己会判断什么时候该收手。

 

 二、流式输出:让字一个个蹦出来

 

第二块要解决的,是"等"的问题。

 

大模型生成回答不是一秒钟出全文的,它是一个 token 一个 token 往外蹦。你要是傻等它生成完再一次性返回,用户得盯着白屏几十秒,还以为程序死了。

 

解决办法特别朴素:后端收到一点,就往前端送一点,前端立刻显示。这就是流式输出,俗称打字机效果。

 

后端就三步:
 
// 1. 告诉浏览器:我要用 SSE 慢慢推数据了
res.writeHead(200, { 'Content-Type': 'text/event-stream' });

 

// 2. 请求模型时打开 stream: true
const stream = await openai.chat.completions.create({ ..., stream: true });

 

// 3. 每收到一块,就写一条 data: 消息
for await (const chunk of stream) {
  res.write(`data:${JSON.stringify(resObj)}\n\n`);
}
 

 

这里用到的协议叫 SSE(Server-Sent Events)。它的格式土得可爱:每条消息以 data: 开头,用两个换行结尾,服务器想推就推,浏览器一直连着不收线。跟 WebSocket 比,它是单向推流,但对聊天这种"服务器一直说、客户端偶尔插嘴"的场景完全够用,而且简单得多。

 

前端这边,用现成的 @microsoft/fetch-event-source 库,几行代码搞定:发起请求,注册一个 onmessage 回调,每收到一块就往聊天框里追加。写完你会惊叹:就这么点东西?

 

顺带说一句,实际项目里后端还会趁流式的机会做点别的事:把回复存进对话历史、消息太长时先让 AI 总结一下再接着聊。但核心永远就是上面那三步,别被花活吓到。

 

 三、AI 的话不能只是"话":md 里嵌套自定义组件

 

流式解决了"快",接下来解决"好看"。

 

AI 的回复默认是一段 markdown:标题、列表、表格、代码块。前端用 markdown 解析库(比如 @crazydos/vue-markdown 配两个插件)就能渲染成规整排版,代码还能高亮。而且这类库一般都支持自定义渲染模板——想把人家的 - [ ] 待办 渲染成真正的复选框而不是一行文字,加个模板就行。到这一步,AI 已经能"说得好看"了。

 

但有些东西,光靠文字真的费劲。比如用户要登录,你让 AI 回复"请把用户名和密码发给我"?又怪又别扭。真正顺滑的体验是:AI 直接甩给你一个登录表单

 

怎么做?一句话:跟 AI 约定一个"暗号"标签,让它把界面描述成 JSON 塞进回复里。

 

我们项目里用的暗号是 <a2ui>。AI 想展示界面时,就在回复里输出:

 

 
<a2ui>
[
  { "createSurface": { "surfaceId": "login_form" } },
  { "updateComponents": { ...组件树... } },
  { "updateDataModel": { ...数据... } }
]
</a2ui>

 

三段 JSON 各司其职:createSurface 是"支起画布";updateComponents 描述组件长什么样(输入框、按钮、文本,用 id 互相引用拼成一棵树);updateDataModel 负责往组件里填数据。组件里的 content 可以不写死,只放一个 path 占位符(比如 /product/name),真实数据统一从 dataModel 里取——骨架和数据分离,改数据不用改结构。

 

后端收到这个流时,会维护一个状态机:平时是 normal 状态,普通文字直接透传;一检测到 <a2ui> 开头,就切进 a2ui 状态,把后面收到的内容往缓冲区里攒,边攒边用括号配对的办法判断"这个 JSON 对象完整了没"。完整一个,就往前端发一个事件:a2ui:createSurfacea2ui:componenta2ui:updateDataModel……

 

前端根据事件类型把组件一个个挂到画布上。于是你能看到:先出来一个骨架屏,然后组件渐进式地一个接一个冒出来,最后数据填充进去。用户点"登录"按钮时,按钮上绑定的 action(连同输入框里的数据)被发回对话,AI 接着处理——界面和对话就这样串成了一条完整的交互链。

 

这就是"AI 返回 md 文件、嵌套前端自定义组件"的展示逻辑:能说清楚的就用文字(md),需要交互的就给组件(a2ui),两条腿走路。

 

四、Agent 的思考方式:ReAct,边想边干

 

聊完"怎么说",聊聊 agent"怎么想"。

 

现在最主流的模式叫 ReAct,全称 Reason + Act,也就是"推理 + 行动"循环。翻译成大白话:

 

1. 模型先:回答这个问题需要啥?"我需要查一下商品库存,调用查库存工具,参数是 X";
2. 程序:真的去执行这个工具,拿到结果;
3. 模型:结果塞回对话,它看到之后再想下一步——可能再调一个工具,也可能觉得够了;
4. 直到信息齐了,输出最终回答,收工

 

代码层面,这玩意就是个递归:

 

 
let response = await 调用模型(messages);
messages.push(response.message);
if (response.message.tool_calls) {
  for (const call of response.message.tool_calls) {
    const result = await 执行工具(call);
    messages.push({ role: 'tool', content: result });
  }
  await 继续循环(messages); // 把结果喂回去,再问模型一次
}
 

 

有工具调用就执行、把结果喂回去、再问一遍,直到模型吐出纯文本。

 

我第一次看到这个循环的时候有点懵:就这?就这。ReAct 厉害在让模型"手脚并用"——光靠脑子里背的知识不够,就动手查;查完接着想。但它也有代价:一个问题可能要来回调好几次模型,token 花得多、人也等得久。所以后来才有了各种优化,比如一次并行调多个工具,或者干脆让程序先想好流程再干活——这就引出最后一块拼图。

 

五、工作流的三种玩法:固定、可编辑、编排

 

当一个聊天助手要干的活超过"一问一答"时,就得把多步操作组织起来,这就是工作流。我见过也写过三种玩法,从死板到灵活排一排:

 

1. 固定工作流:流水线

 

程序里把步骤写死:查攻略 → AI 分析推荐什么品类 → 查商品接口 → AI 流式输出最终推荐。每一步执行完,给前端发一个"现在进行到哪一步"的通知,界面上就能出现进度效果。

 

优点:稳。每一步都可预期、可调试、可兜底(接口挂了还能降级用本地数据)。缺点也明显:流程要改,就得改代码、重新发布。适合业务非常固定的场景。

 

2. 可编辑工作流:把流程变成配置

 

改一次代码太痛苦,那就把"节点和连线"从代码里抽出来,放进一个 JSON 配置文件。前端做个可视化编辑器,像搭积木一样拖节点、连线、配参数;后端放一个引擎,读这份配置、按顺序执行节点。节点之间传数据用 ${nodeResults.xxx} 这样的变量引用。

 

从此改流程不用碰代码,不会写代码的同事都能上手。适合流程频繁调整的团队。

 

3. 编排模式:让 AI 自己当导演

 

再激进一点:连流程都不提前定,让一个"主编排 Agent"上场。用户问题来了,先让主编排 Agent 分析,输出一份执行计划 JSON——用哪些节点、什么顺序、每个节点传什么参数。然后程序照着计划逐个执行节点,每个节点的结果合并进一个上下文对象;后续节点的参数里写 {userQuestion}{products} 这样的模板变量,执行前自动替换成前面攒下来的数据。

 

相当于 AI 现场组队:这个问题复杂,多上几个节点;那个问题简单,一个节点就够。

 

灵活是真灵活,代价也真实:每次请求先多花一次"规划"的钱和时间,而且规划可能失误(比如调了不该调的节点),得做好兜底。

 

怎么选? 我的土办法:业务简单且长期不变 → 固定工作流,别瞎折腾;业务三天两头变 → 可编辑工作流;需求五花八门、想追求"智能感" → 编排模式,但记得兜底。

 

写在最后

 

整套搭完回头看,我最深的感受是:AI 应用没有玄学,全是朴素的工程。

 

Agent 是个循环,Tool 是带说明书的函数,Skill 是沉淀下来的操作手册;流式输出是"边生成边发送",嵌套组件是"约定标签 + 状态机解析 + 事件驱动渲染";工作流就是把步骤组织起来,组织方式从写死到配置到 AI 规划,灵活度递增、稳定性递减。

 

真正难的从来不是某个点,而是把这些点串成一个完整、稳定、体验顺滑的产品。希望这篇啰嗦的长文,能帮你少走一点我当时走过的弯路。

关于Webfunny

Webfunny专注于前端监控系统,前端埋点系统的研发。 致力于帮助开发者快速定位问题,帮助企业用数据驱动业务,实现业务数据的快速增长。支持H5/Web/PC前端、微信小程序、支付宝小程序、UniApp和Taro等跨平台框架。实时监控前端网页、前端数据分析、错误统计分析监控和BUG预警,第一时间报警,快速修复BUG!支持私有化部署,Docker容器化部署,可支持千万级PV的日活量!

  点赞 0   收藏 0
  • metal
    共发布10篇文章 获得0个收藏
全部评论: 0