做内部 agent 的时候有一个很自然的冲动:既然模型会用工具了,那就把工具都给它,让它自己决定怎么办。

我们最后没这么做。核心的那条链——「把一段对话变成一张填好的工单并提交」——是写死顺序的:

打开表单 → 抓取会话内容 → 调模型生成字段 → 填进表单 → 校验 → 提交 → 确认真的生成了

模型只在第三步出现,负责「把一段对话写成结构化的字段」。它不决定要不要提交,也不决定下一步做什么。

这篇讲为什么。

两种形态的真实差别

固定流程 + AI 节点function calling 自主编排
每一步做什么写死在代码里模型推理出来的
出问题时能定位到第几步只能看它的推理链
能单测吗能,每一步都是普通函数只能端到端跑
成本一次模型调用N 轮,N 不确定
改一步改一个函数调 prompt,然后祈祷
做错时的范围那一步的范围它能调到的一切

最后一行是关键。

我们的规范里有一句:「录单可以自主提交,对客户发消息永远人确认。」接 function calling 的时候,这句话差点被读成「所以提交工单这个能力可以给模型自主调」。

不是一回事。

「录单可自主提交」说的是那条写死的链——输入确定(一段对话)、输出确定(一张工单)、每一步都能验证、出错能精确定位。它里面的 AI 环节只负责生成文本。

而 function calling 是模型自己决定调什么。坐席在侧栏问一句「这个客户之前报过类似问题吗」,模型完全可能在查完之后顺手调一下提交——在它的推理链里,那看起来是个合理的下一步。

「一条固定流程可以自主」和「一个 agent 可以自主」,中间隔着的是可预测性。

出问题时你能问出什么问题

这是我最在意的一点。

固定流程出问题,我能问的是:「它卡在第几步?」 然后去看那一步的日志、那一步的输入输出、那一步的返回值。

我们那条链里每一步都有明确的失败态:

  • 表单没打开 → 没找到录单字段
  • 会话抓不到 → 会话内容为空(并且要区分是「接口失败」还是「真的没内容」)
  • 模型调用失败 → 模型超时 / 返回不是合法 JSON
  • 字段没填全 → 缺:问题描述
  • 提交失败 → 接口返回的原话

这五类各有各的修法,而且大部分不需要碰 AI。实际运行里,真正由模型引起的失败是少数。

如果是自主编排,同样的失败只会呈现成一句「它没能完成这个任务」,然后你去读一长串推理,猜它当时在想什么。

(关于「错误信息必须对应真实分支」,我们有过一次很贵的教训:一个函数四个失败分支,调用方把所有失败报成同一句话,结果排查方向错了整整四轮。细节在排查方法论。)

固定流程里也有「判断」,只是判断被收窄了

写死顺序不等于没有分支。这条链里有好几处判断,但每一处都是明确定义的小问题:

// 提交策略闸:不是「模型觉得该不该提交」,而是一组可以列举的条件
if (!hasRequiredFields(form)) return saveDraft('字段不全');
if (!isValidCustomer(customer)) return saveDraft('客户无效');
if (policy === 'draft-only') return saveDraft('策略要求只存草稿');
return submit();

AI 参与的那一步也一样:它的输入是「这段对话」,输出是「这几个字段」,有 schema。返回的不是合法结构就当失败重试,不会「部分成功」。

把 AI 放在一个输入输出都确定的位置上,你就还能用普通软件工程的手段对待它:重试、校验、单测、埋点。

一旦让它决定流程,这些手段全都失效了。

那 function calling 我们用在哪

用了,但只放开 read。

侧栏里的对话助手可以自主调只读工具——查历史记录、检索知识库、读当前会话。这些做错的成本是「答得不对」,而不是「改了不该改的东西」。

三层防护保证它出不去(细节在 tool 的副作用分级):

  1. 给模型的工具清单里根本不含 write / send 类;
  2. 执行出口再查一次副作用等级——防它幻觉出一个不存在但看起来合理的名字;
  3. 注册表本身的三道闸。

两条路并存,不替换。 固定流程是默认与回退,自主编排是新增的可选路径。

什么时候才放开 write

不是「等模型更聪明」,是等三件事做完:

① 目标操作要有真正的 API。 现在提交工单走的是 DOM 操作——填表单、点按钮。这条路没有幂等性,也没有「预演一次看看结果」的可能。有了 API 才谈得上让模型去调。

② 要有「模型提议 → 人确认卡片 → 执行」的交互。 不是弹个 confirm,是把模型打算做什么完整展示出来:要提交的是哪张单、填了什么、影响谁。人看完再点。

③ 失败要能回滚,或者至少能知道做到哪了。 现在的链路出错会留下半填的表单,这在人工流程里没问题(人能看见),在无人值守下就是脏数据。

这三件没有一件和模型能力有关。它们都是把不可逆变成可逆、把不可见变成可见。

一个具体的例子:批量补录

我们最重的那条链是批量的——扫描一批历史会话,逐个补录工单。

它完全没有 function calling。整个流程是一个 for 循环,每一轮调同样的函数、走同样的分支。模型只在「写工单内容」那一步被调用一次。

这条链上踩过的坑,没有一个能靠换个更聪明的模型解决:

  • 列表是懒加载的,不滚到底就只能扫到当前渲染出来的那些(实测 361 → 447 条,还在继续加载);
  • 提交之后宿主要异步关弹窗、异步刷新列表,不等它结算就切下一个,会撞进「上一张表单正在重建」的抖动态;
  • 多个 frame 各跑一份实例,一起抢同一张表单;
  • 后台的模板轮询会把已填好的内容覆盖成空。

这些全是工程问题。而它们在一个自主编排的 agent 里会表现成「它有时候能成功有时候不能」——然后你会去调 prompt。

我最担心的不是 agent 做错事,是它把工程问题伪装成智能问题。


我不认为固定流程永远是对的。等前面那三件事做完,放开 write 是自然的下一步。

但在那之前,「写死顺序」不是保守,是一种让问题可被定位的设计。

模型很强这件事,不改变「出了问题得有人修」这件事。而修的前提是你知道它坏在哪一步。