查找一份资料,可以按固定关键词查询,也可以在没有结果时换一个词继续查。实现成脚本、工作流或 Agent,区别不只是有没有调用模型,还包括下一步由谁决定。
这里用同一个任务比较:查询 Cookie 文档,如果没找到,尝试调整关键词。数据只有两条,不连接外部服务。Agent 部分用模拟决策器代替模型,方便检查执行过程。
脚本与工作流
脚本也可以有复杂分支,工作流也可以由代码实现,两者没有严格的代码形式分界。这里把一次固定查询作为脚本,把预先规定的“失败后换词”作为工作流:
const notes = [
{ id: 'cookie', title: 'Cookie', text: '浏览器携带会话 ID。' },
{ id: 'stream', title: '流式请求', text: '逐步读取响应并支持取消。' },
];
function search(query) {
const keyword = query.toLowerCase();
return notes.filter(note =>
(note.title + note.text).toLowerCase().includes(keyword)
);
}
function runScript(query) {
return search(query);
}
function runWorkflow(query, fallbackQuery) {
const result = search(query);
return result.length ? result : search(fallbackQuery);
}
工作流根据结果选择分支,但“没有结果就用 fallbackQuery”是开发者写好的规则。流程里使用一次模型改写文字,也不会自动让整个流程变成自主选择工具的 Agent。
步骤稳定时,固定规则往往已经够用。是否引入模型,要看是否有难以提前列全的决策,而不是代码里出现了循环或多个函数。
决策器和执行器
Agent 的常见实现是让模型结合目标和已有结果,决定下一步动作。执行器负责检查动作是否合法、调用工具,再把结果交回决策器。
这个结构可以不连接模型就检查。把下面代码接在前面的查询函数之后:
async function runAgent(decide, { maxSteps = 4 } = {}) {
const observations = [];
const trace = [];
for (let step = 0; step < maxSteps; step++) {
const action = await decide({ step, observations });
if (action?.tool === 'finish') return { done: true, trace };
const query = action?.args?.query;
if (action?.tool !== 'search' || typeof query !== 'string' ||
!query.trim() || query.length > 80) {
throw new Error('不允许的工具或参数');
}
const result = search(query);
observations.push(result);
trace.push({ action, result });
}
return { done: false, trace };
}
执行器只接受 search 和 finish。决策器返回的内容不拿来 eval,也不允许它通过自定义工具名调用任意程序。达到步数上限时返回未完成,而不是无限循环。
done: true 只表示决策器选择了 finish,不自动证明任务完成正确。真实应用还需要校验最终答案或任务结果。
用模拟动作观察过程
把前面两段代码和下面代码放在同一个 compare.mjs 中,执行 node compare.mjs:
import assert from 'node:assert/strict';
console.log('脚本:', runScript('missing'));
console.log('工作流:', runWorkflow('missing', 'Cookie'));
const actions = [
{ tool: 'search', args: { query: 'missing' } },
{ tool: 'search', args: { query: 'Cookie' } },
{ tool: 'finish' },
];
const result = await runAgent(async ({ step }) => actions[step]);
assert.equal(result.done, true);
assert.equal(result.trace[0].result.length, 0);
assert.equal(result.trace[1].result[0].id, 'cookie');
console.log('模拟决策:', JSON.stringify(result, null, 2));
await assert.rejects(
runAgent(async () => ({ tool: 'write', args: {} })),
/不允许/,
);
const unfinished = await runAgent(
async () => ({ tool: 'search', args: { query: 'missing' } }),
{ maxSteps: 2 },
);
assert.equal(unfinished.done, false);
assert.equal(unfinished.trace.length, 2);
console.log('非法动作和步数上限检查通过');
脚本返回空数组,工作流按预设词找到 Cookie,模拟决策器按三步动作结束。模拟动作提前写好了,并没有根据观察自主选择,所以这个结果只能检查执行器,不能证明模型会自己调整查询。
接真实模型时,decide 可以把目标和 observations 传给模型,再把返回内容解析成受约束的动作。模型输出仍然需要校验,不能因为使用结构化输出,就跳过工具权限和参数检查。
增加模型之后还要控制什么
步数限制只能控制调用次数,不能限制单次调用卡住多久。实际应用还需要单步超时、总时间、费用预算和中止信号。
工具有写入或付款等副作用时,要明确权限及确认流程,并处理重试导致的重复操作。查询结果也可能来自不可信网页,不能把页面中的文字提升成控制工具的指令。
评估真实模型,需要用固定任务集记录任务结果、工具调用次数、费用和失败类型。一次完成的演示,不足以说明它比预设流程更可靠。
对于这里的固定查询任务,一条 fallback 规则就能完成。查询范围和策略经常变化,难以提前写全分支时,模型决策才可能带来帮助,同时也增加了需要检查的行为。