上周我们内部上线了一个功能:用户在前台对话框里一次性说出“帮我查一下订单TX2023981的物流,如果已经签收,就给收货人发一条确认短信,另外把下周一的会议从上午十点改到下午三点”,系统能把这件任务自动拆成三个独立动作,在后台并行执行,前台实时滚动显示进度。这个功能上线后,我对“AI办事”这件事的理解彻底变了——单纯把大模型接入对话窗口,得到的只是一个“会聊天的窗口”;只有踩过调度、状态、回传这一堆坑之后,才能把它做成一个“会办事的入口”。
这篇文章想聊聊我们是怎么基于某高校实验室开源的9B实时对话模型(下文统一叫它 Realtime-Venus),自己搭了一套 Harness 编排层,把前后台打通的。所谓“前后台”,前台是用户的实时对话流,后台是订单、会议、审批这些业务系统。所谓 Harness,是我们内部对“对话模型与业务系统之间那层调度与控制逻辑”的称呼。文章会先讲清楚为什么必须额外做这一层,再说模型选型的考虑,然后把异步并行任务编排的核心设计拆开,最后给出一路走来的踩坑记录。如果你也在折腾 AI Agent、智能客服升级,或者想把大模型接到企业内部系统里,这篇应该对你有用。
1. 为什么“会对话的模型”不等于“会办事的系统”
1.1 对话层与业务层之间的断层
大模型最擅长的事情是生成自然语言,但企业内部要的从来不是“一段漂亮的回复”,而是“一个确定的结果”。用户说“把订单改成明天送达”,模型能听懂,但真正完成这个动作需要调用仓储系统的改单接口、校验改单时限、匹配承运商、更新物流单号,任何一个环节出错,回复再流畅也没有意义。
我们早期踩过一个很典型的坑:让模型直接生成后端接口的入参,程序拿到参数后同步调用。单动作场景看起来没问题,但需求稍微复杂一点就露馅了——用户说“查一下A订单物流,同时给B客户发一封催款邮件”。同步调用意味着阻塞式等待,快递查询可能要两三秒,催款邮件接口可能要五秒,用户在前台盯着光标转圈,整个对话的交互节奏全被拖垮。
对话系统的本质是“轮次”,后台任务的本质是“流程”。对话要求即时响应,后台流程可能持续几秒甚至几分钟,两者时序天然不匹配。这个断层靠模型本身是补不上的,必须在中间加一层专门干“调度”的组件,也就是我们说的 Harness。
1.2 Harness 到底是个什么角色
Harness 的本意是马具、挽具,我们拿这个词来命名编排层,含义很直白:让模型这颗“大脑”能拖着后台业务这辆“大车”跑起来。它的核心职责包括:接收模型输出的结构化意图、对意图做合法性校验、把意图拆成可执行的任务、按依赖关系调度任务、监控任务全生命周期,最后把执行结果汇总回对话上下文,供模型生成下一轮回复。
很多人听到这里会问:这不就是 Function Calling 吗?区别其实很大。Function Calling 解决的是“这一轮对话中,模型决定调用哪个函数、传什么参数”,它本质上是单轮的“按下按钮”。而 Harness 要处理的是跨多轮对话、管理多个并行动作的状态流转,它相当于一条完整流水线的控制台和仪表盘。你在对话里交代了三件事,其中一件卡住了,你要能单独看它的进度,而不是整段对话一起死掉。
这套 Harness 从架构上划分为四个组件:意图解析器、任务调度器、执行器、上下文管理器。意图解析器负责把模型的输出翻译成结构化任务;任务调度器控制任务队列和并行度;执行器对接后台业务接口;上下文管理器负责把任务结果写回对话记忆。
1.3 先划边界,再谈编排
项目启动时我们做了一个看起来很“保守”的决定:只允许三类动作被 Harness 接管。第一类是查询类,包括查物流、查余额、查审批进度;第二类是变更类,包括改预约、改订单、提交审批;第三类是投递类,包括发短信、发邮件、触发站内通知。除了这三类,其他内容仍然由模型自由对话,Harness 不插嘴。
这个边界划分帮了大忙。Harness 不需要理解整个企业业务,只需要理解三种动作的共性:输入参数长什么样、动作语义是什么、返回结果怎么解析、有没有副作用。边界清晰之后,模型的意图解析压力也小了,它只需要在“对话”和“办事”两个模式之间切换,而不是在一堆复杂的业务规则里猜来猜去。
2. 模型选型:为什么选 9B 开源的 Realtime-Venus
2.1 9B 参数在性价比上的胜利
我们一开始也考虑过更大的模型,但实时对话场景有两个硬约束绕不过去:首字延迟和吞吐成本。9B 模型量化之后显存占用大概在 6 到 8GB,一块中端 GPU 就能跑,单用户并发下首字延迟通常可以压到 800 毫秒以内。作为对照,几百 B 的模型回答质量确实更高,但同样的硬件条件下首字延迟很容易飙到两三秒,多用户并发时显存和带宽完全不够用。
成本也很好算。一套系统只需要一两张卡部署,不会出现“模型运行成本比业务本身还贵”的尴尬。而且 9B 模型经过微调后,在“工具调用格式遵循”和“业务术语理解”这两个关键维度上,表现完全够用。我们内部专门做过一轮评测,结论是:在短任务指令、多意图拆解这类场景里,9B 与更大模型的差距远没有想象中那么大。
2.2 “实时”两个字不是噱头
Realtime-Venus 的训练目标就是“边生成边决策”。普通对话模型往往要生成完整回复后才交给你解析,而 Realtime-Venus 能在流式输出的过程中直接输出结构化的工具调用标记。这一点对“办事”场景太关键了。
我们用普通模型搭配 Function Calling 做过对比,最头疼的问题是两个:一是多任务混在一句话里时,模型很容易漏掉某个子任务,比如“查物流、发短信、改会议”三个动作,它偶尔只识别出两个;二是参数槽位填得模棱两可,时间、单号、收件人这些字段经常缺胳膊少腿。Realtime-Venus 在训练阶段就强化了流式结构化输出,对多意图、多工具的指令跟随稳定性明显高一截。
另外,这个模型的长上下文利用率做得不错。实际业务里经常出现这样的对话:用户先交代了任务A,中间聊了些别的,过了几轮突然问“刚才那个任务怎么样了”。这时候模型必须能从上下文中准确捞回任务A的状态,而不是像金鱼一样七秒记忆。这个模型在这方面没有让我们失望。
2.3 私有化部署带来的数据与链路优势
开源模型最大的好处是可以完全部署在内网,数据不出门。我们接的企业内部系统对数据安全要求极高,直接调公有云API基本过不了合规评审,这一条就决定了我们只能选开源方案。
本地化部署还带来一个容易被忽略的优势:链路更短。从对话层到 Harness 再到后台系统,全程内网通信,单跳延迟非常低。如果依赖外部API,每做一次工具调用的网络往返可能就要多出几百毫秒,异步并行任务最后汇总时,体验会明显变差。
当然,可微调是加分的另一个点。我们用业务语料对 Realtime-Venus 做了一次领域微调,重点教它识别内部特有的系统名称、单据编号格式和操作动词。效果非常直接:工具调用字段的格式错误率下降了一个量级。
3. Harness 核心架构:异步并行任务编排的关键设计
3.1 从一句话到多任务列表:任务拆解的逻辑
用户一句话里可能包含多个意图,Harness 拿到模型的流式输出后,第一步不是执行,而是把输出解析成一份“任务清单”。我们定义了一个中间表示,每个任务包含五个字段:action(动作名)、params(参数字典)、dependencies(依赖的任务ID集合)、timeout(超时时间)、retryCount(当前重试次数)。
解析要过两关。第一关是 schema 校验,检查字段类型对不对、必填项有没有、值的枚举范围是否合法。比如“把会议改到下午三点”,“下午三点”必须能被解析成标准时间格式,否则直接打回让模型重新生成。第二关是依赖分析,判断任务之间有没有先后关系。比如“如果物流已签收就发短信”,发短信这个动作依赖快递查询的结果,必须串行等待;“查物流”和“发催款邮件”之间没有依赖,可以进并行池。
依赖关系我们用一张有向无环图(DAG)来管理。无依赖的任务放进并行池,有依赖的任务按拓扑顺序排列。项目初期不需要上完整的工作流引擎,一张 DAG 加一个拓扑排序就够用了,做得太重反而拖慢开发节奏。
3.2 异步调度:队列、状态机与生命周期管理
并行池里的任务通过消息队列分发。我们的实际方案是:请求进来后,Harness 先把任务写入 Redis List,一组 Worker 进程从队列里拉取任务执行,执行结果写回 Redis,并通过事件通道广播状态变化。
为什么不直接用同步线程池?因为任务几乎都涉及外部API调用,耗时不可控,一个慢请求很容易把线程池拖垮。用消息队列解耦后,任务生产者(对话请求)和任务消费者(Worker)可以独立扩缩容。高峰期并发上来了,多开几个 Worker 就行。
每个任务都有完整的生命周期状态:pending(排队中)、running(执行中)、succeeded(成功)、failed(失败)、cancelled(已取消)。这个状态机是 Harness 的核心,所有状态流转都必须带时间戳记录。排查问题时最有用的就是这行日志:任务X在 09:01:12 入队,09:01:15 开始执行,09:01:42 失败,失败原因是什么。有了它,定位卡点基本不会超过五分钟。
class TaskStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELLED = "cancelled"
并行度控制也是一个容易忽略的细节。我们最初让所有无依赖任务同时跑,结果大量请求同时打到老旧后台系统,把数据库连接池打满了,一堆任务报错超时。后来加了信号量限流,默认最大并行数设为4,压测结果反而更稳定,整体吞吐不降反升。这个数字不是拍脑袋定的,是根据后台系统连接池大小和平均响应时间算出来的。
3.3 结果回传:对话上下文如何感知任务完成
异步任务跑完后,结果必须回到对话上下文里。否则用户等了一会儿问“查得怎么样了”,模型对执行结果一无所知,这个交互就断链了。
我们的设计是:每个会话维护一个上下文容器,Worker 执行完任务后,通过事件回调把结果写入容器的“任务结果区”。当模型准备生成下一轮回复时,Harness 会把任务结果区的内容拼接进提示词。
多任务并行时,我们还做了阶段性汇总推送。比如用户同时发起三个任务,前端会收到类似这样的进度消息:第一个查询已完成,第二个邮件发送中,第三个正在排队。这种进度反馈对异步体验至关重要,它向用户传递了两个信息:系统确实在干活,系统没有死。没有进度反馈的异步,在用户眼里等同于卡死。
4. 前后台打通的关键路径与工程细节
4.1 前端实时交互层:SSE 与 WebSocket 分开用
模型生成回复用的是 SSE(Server-Sent Events)流式推送,后台任务状态变化用的则是 WebSocket,两条通道独立,互不干扰。
一开始我们图省事,把所有消息塞进同一条 WebSocket 通道,结果出现一个很尴尬的情况:模型生成的文字流和任务状态更新互相抢顺序,前端渲染乱成一团。后来拆成两条独立通道,SSE 专门跑模型 token 流,WebSocket 只负责推送任务状态事件,问题立刻解决。
前端要维护三种状态:模型生成中显示打字机效果,工具执行中显示进度动画,任务完成弹出结果卡片或汇总消息。这听起来简单,实际开发中最麻烦的是处理时序对齐,尤其是多个任务几乎同时完成的时候,汇总卡片要有顺序、有主次,不能乱跳。
4.2 后台业务接口的统一契约
Harness 不能直接依赖某个业务系统的私有接口,否则每接入一个新系统都要改一遍 Harness。我们定义了一个“动作注册表”,每接入一个可执行动作,就往注册表里填一份配置。
{
"actionName": "create_meeting",
"inputSchema": {
"room": {"type": "string", "required": true},
"startTime": {"type": "datetime", "required": true},
"endTime": {"type": "datetime", "required": true}
},
"outputSchema": {
"meetingId": {"type": "string"}
},
"timeout": 30000,
"idempotent": true,
"retryable": false
}
这份配置里最重要的字段是 timeout、idempotent 和 retryable。timeout 决定任务调度器最多等多久;idempotent 告诉 Harness 这个动作能不能安全重试;retryable 表示失败后是否按指数退避策略自动重跑。业务系统只需要按这个契约实现标准接口,保证入参出参符合 schema,Harness 不关心业务系统内部到底怎么实现。
这种面向契约的接入方式,让对接新系统的成本大幅下降。我们中期接入了第三个业务系统,整个接入只花了两天,大部分时间都花在写动作实现和联调上,Harness 的代码一行都没改。
4.3 并行任务的幂等、去重与一致性处理
并行任务最大的隐患是“重复操作”。用户可能在网络抖动时点了两次发送,或者模型在一次输出里给出了两个意图完全相同的动作。如果系统傻乎乎地执行两次“扣款”“发邮件”,就是事故。
我们的处理分三层。第一层是全局任务ID作为幂等键。Harness 在拆解任务时给每个任务生成 UUID,写入 Redis 的去重集合。业务接口收到请求后先查幂等表,如果任务 ID 已经处理过,直接返回上次的执行结果,不再触发业务逻辑。
第二层是针对有副作用的操作,采用“预检-确认-提交”三段式。先让业务系统做一次预检,比如检查订单当前状态是否允许修改;确认一切正常后,再提交真正的变更。这样能避免“反查状态时是正常的,真正执行时状态已经变了”的竞态问题。
第三层是超时清理。异步任务如果长时间卡住,必须有超时机制把它标记为 failed,并通知用户“这个任务失败了,可以重试”。我们当前给查询类任务设 30 秒超时,变更类任务设 60 秒,投递类任务设 90 秒,这几组数字都是根据真实业务统计后调整出来的。
5. 踩坑记录:异步并行实施中的典型问题与排查技巧
5.1 常见问题速查表
| 现象 | 根因 | 解决方案 |
|---|---|---|
| 服务重启后队列里的任务全部消失 | 任务只存在内存队列里 | 引入 Redis 持久化,启动时从 Redis 恢复未完成任务 |
| 多个任务同时修改同一订单,后写的覆盖先写的 | 并行任务间存在资源竞争 | 对同维度业务对象加分布式锁,按订单ID分桶串行化 |
| 模型输出的 JSON 结构不稳定,解析频繁失败 | 小模型对复杂结构化输出跟随性不足 | 增加 schema 校验,失败后让模型结合错误信息重生成,必要时拆成更短指令 |
| 前端收到的消息顺序错乱 | SSE 与 WebSocket 共用通道互相干扰 | 拆分为两条独立通道,SSE 只跑模型流,WebSocket 只跑任务状态 |
| 任务执行成功但对话里没体现 | 任务结果没有写回上下文容器 | 建立统一的任务结果回调机制,生成回复前自动拼接任务结果区 |
| 高并发下后台系统连接池被打满 | 并行度设置过高 | 增加信号量限流,把最大并行数调低,观察系统吞吐拐点 |
| 9B 模型漏拆用户指令中的某个子任务 | 模型对多意图识别能力有限 | 在提示词里加入 few-shot 示例,明确要求“先列出所有动作再逐项执行” |
5.2 三个值得注意的实践细节
第一,任务 ID 必须贯穿全链路。从模型输出解析、队列入队、Worker 执行、业务API调用到前端推送,所有日志都要带上任务 ID。排查问题时,一条命令按任务 ID 把所有日志捞出来,效率提升是巨大的。没有全链路追踪,异步系统出问题时就只能靠肉眼猜。
第二,给用户看的进度文案要精确。不要出现“AI 正在思考”“后台正在处理”这类空洞话术,最好精确到“正在执行:会议室预订(排队中)”。这个体验细节虽然小,但直接决定了用户对系统“靠不靠谱”的判断。
第三,单元测试一定要模拟乱序完成。不同任务的完成顺序是随机的,结果回传后上下文拼接必须保持稳定。我们在测试套件里专门写了“完成顺序随机化”的用例,逼出了不少竞态 bug。异步系统的测试思维和同步系统完全不同,不能想当然地假设“先发起的任务先返回”。
我个人在实际操作中的体会是,模型的能力占比远没有想象中那么高,真正决定“AI 办事”体验的,是 Harness 里那一堆不起眼的细节:状态怎么流转、错误怎么恢复、进度怎么告诉用户。踩过几次坑之后,我愈发觉得异步并行不是技术炫技,而是对用户时间的尊重。后续我们准备把 Harness 沉淀成一套可配置的流程编排界面,让业务运营自己定义新的“办事动作”,而不是每次都由研发介入。这个方向如果跑通了,AI 办事的下一个门槛就不是技术,而是业务想象力了。
125




被折叠的 条评论
为什么被折叠?



