我第一次让 AI 自己干活,用的是最朴素的方式:一次 API 调用,带上工具列表,模型调用一次工具,我执行一次,把结果塞回去,结束。后来任务变复杂了,要查三份资料、跑一段代码、再汇总成报告,我就发现真正的问题不是模型聪不聪明,而是另一层从来没想过的事:工具什么时候给、任务怎么拆、每一步的结果放在哪。
这一层,就是 Agent 编排(智能体本体是什么,此前有篇 AI Agent 入门)。它不写在模型里,也不写在单个工具里,它是你作为开发者在模型外面包的那层设计。今天把这三层拆开讲清楚。
先把两个词掰开:workflow 和 agent
Anthropic 在那篇被引用最多的《Building Effective Agents》里画过一条线:workflow 是 LLM 和工具沿着预先写好的代码路径被编排;agent 则是 LLM 动态指挥自己的过程和工具使用,自己掌握完成任务的方式。
区别说白了就一句话:流程写在代码里,还是写在模型脑子里。固定流程的,比如收到问题就检索、检索完就按模板总结,这是 workflow;模型自己决定下一步查什么、要不要跑代码、跑完够不够,这是 agent。两者都属于「agentic systems」这个大类,只是控制权归属不同。
而不管 workflow 还是 agent,地基都是同一块。Anthropic 叫它 augmented LLM:在普通 LLM 上加了检索、工具、记忆这些增强件,让模型能自己生成检索词、自己挑工具、自己决定记什么。你手里的每一个智能体,拆到底都是这块积木,编排就是把一块积木变成一个系统的方法。
工具编排:工具不是越多越好
第一个常见的坑,是把所有工具一口气全塞进请求。
工具从哪来?此前写过 Function Calling 工具调用的完整流程。而 Kimi 官方文档给这个现象起了名字,叫工具定义膨胀(Tool Definition Bloat)。每个请求都要携带全部工具的名字、描述和参数 schema,token 消耗涨得快;更糟的是候选工具一多,模型反而更容易选错工具、构造出错误的参数。工具到了几十上百个的量级,这几乎是必然。
现在的解法是按需上架,有三招。
第一招,动态加载工具。先只挂少量核心工具,对话进行到需要某个工具时,再把它的完整声明追加进对话。Kimi 的做法是在 messages 里插入一条带 tools 字段的 system 消息,工具从这条消息所在的位置开始对模型可见,注入的声明格式与顶层 tools 完全一致,不需要维护两套 schema。
{
"role": "system",
"tools": [
{
"type": "function",
"function": {
"name": "create_github_pr",
"description": "在指定仓库创建 Pull Request",
"parameters": { "type": "object", "properties": {} }
}
}
]
}
第二招,tool search 模式。API 层面没有现成的工具搜索接口,官方推荐自己拼:顶层只声明一个 search_tools 工具,由后端实现,按关键词返回候选工具的名字和简介;模型需要工具时先调它,你的应用再把命中的工具完整声明注入对话。这样不管你手里有一千个工具,每一轮请求里实际出现的声明只有几个,上下文和选择压力都可控。
成套的工具和流程还可以打包成技能交给 AI 自己取用,此前写过 Agent Skills 原理。第三招是 tool_choice 强制首轮检索。模型可以不调任何工具、直接凭记忆回答,为确保它先检索再干活,首轮请求把 tool_choice 设为 required,检索完成后再恢复 auto。修改 tool_choice 不会破坏前缀缓存,可以按请求粒度单独调。
追加,不要插入
动态加载工具和提示缓存天生要配合用,官方文档里最重要的一条纪律就六个字:追加,不要插入。
前缀缓存按前缀匹配,前缀里任何位置变了,那个位置之后的缓存全部失效(各家的配置方法此前在 提示缓存实操里写过)。所以有三条原则:新的工具声明一律追加到 messages 末尾,已有前缀保持不变;已注入的声明在后续请求里原样保留,动态声明按请求生效,服务端不记住它们;每轮都用的核心工具固定放顶层 tools 字段,声明之后不再改动。
工具编排这一层,本质上是在回答一个问题:模型每一步「看得见」哪些工具。
任务编排:一条线程,还是一组线程
工具理顺之后是第二个问题:一个任务要不要拆,拆给谁。
不拆的情况,就是一个 agent 循环:模型想一步做一步,调工具、看结果、再决定下一步,循环到任务完成。这个骨架此前写过一篇 ReAct 循环,单线程循环能覆盖大多数任务。
要拆的情况,Kimi 托管智能体的文档里列了几种典型:并行处理互不依赖的资料或子任务;不同子任务需要不同配置的智能体,比如代码审查交给带专用工具的;同一类任务开多个副本分别处理不同输入;需要独立查看每个子任务的执行轨迹。
拆开之后是什么结构?上线前怎么验收,此前整理过 多 Agent 上线清单,这里先看结构。一个协调线程管全局,它决定把任务委派出去,同一个会话里生成子线程,子线程执行完把报告交回来。有几个容易忽略的细节,都来自官方文档:
- 子线程不能再生子线程,层级只有一层。委派名单上限 20 个条目,不允许引用已经配置了委派的智能体,嵌套在写入时就会报错。
- 消息都经过协调线程中转。协调线程可以给指定子线程发消息,也可以广播;子线程只能把结果或问题发回协调线程,子线程之间不能直接通信。
- 委派分同步和异步。同步委派会等子线程干完、拿到报告再继续,适合后续步骤依赖结果的场景;异步委派是派出去就返回,子线程干完一轮把报告送回来,协调线程可以继续干别的。
还有一条值得抄在备忘录上的话:如果任务不需要拆分,直接用普通智能体即可。多智能体编排会增加配置、线程管理和事件观察的复杂度,不应为了使用多个智能体而拆分简单任务。拆不拆,看任务边界清不清楚,不看你有多少个智能体可用。
运行时编排:这些脏活谁来干
编排的最后一层最容易被低估:运行时。自建 agent 系统时,你的代码要负责五件事:自己拼接、存储并回传消息历史;自己解析工具调用、执行并回填结果;自己准备跑命令、读写文件的沙箱环境;自己处理崩溃和断线后的状态重建;自己实现超时重试和限流退避。
Kimi 的迁移文档把这五件事列成对照表,结论是托管平台把「怎么运行」接走,你只保留「是什么任务」。这张表本身就是一份不错的自检清单:哪怕你不打算迁移,也可以照着检查自己的系统里这五件事有没有着落。哪一件没着落,哪一件就是半夜报错的候选。
收个尾
Agent 编排说到底就是三层清单:
- 工具层:核心工具固定顶层,长尾工具按需注入,追加不插入,守住前缀缓存;
- 任务层:能一条线程干完就不拆,要拆就明确同步还是异步,消息走协调线程,不做嵌套;
- 运行时层:历史、工具执行、沙箱、恢复、重试,五件事各有着落,不管是自己写还是平台托管。
工具层和缓存怎么配合,此前写过提示缓存配置的实操;任务拆完之后多智能体怎么上线,也有一篇实战清单可以接着看。从单次 API 调用走到完整的智能体系统,中间隔的不是模型能力,是这几层编排的功夫。