第一周:什么是 AI Agent —— workflow 与 agent 的分界

从零写一个 agent 循环只要一个 while 和一次工具调用。真正的分界不在代码量,在「谁决定下一步」——而这个决定同时决定了你能不能复现结果。

位置
第 01 篇 / 共 6 篇
预计
5 分钟

第一周要回答的问题只有一个:什么是 agent,它和 workflow 差在哪。

这个问题看着像术语之争,其实决定了后面所有的设计。

分界线:谁决定下一步

Anthropic 那篇 Building Effective Agents 给的划法最干净:

  • Workflow —— LLM 被编排在预先写好的代码路径
  • Agent —— LLM 自己指挥流程和工具使用

区别不在复杂度,不在有没有工具,也不在用了哪个框架。**只在控制权在谁手里。**这条判据一旦钉死,「该用哪个」这个问题就变得很好答

一个调了十个工具、跑了二十步的东西,如果每一步走哪儿都是代码写死的,它是 workflow。一个只有两个工具的东西,如果每一步都由模型选,它是 agent。

三种基本模式,都还不是 agent

第一周会过一遍这几个设计模式,而它们全都是 workflow

模式 形状 谁决定
Chaining 串联 A 的输出喂给 B,B 喂给 C 代码
Routing 路由 先分类,再送到对应的处理分支 模型分类,代码路由
Orchestrator 编排 一个主控拆任务,分给若干子任务 主控(可能是代码,也可能是模型)

只有当编排器本身是模型、而且它每一轮都能重新决定派谁去干什么的时候,才越过了那条线。

这三种模式覆盖了绝大多数真实需求。 Anthropic 那篇的建议是只在能证明更好的时候才加复杂度 —— 而 agent 的复杂度成本,主要不是代码难写,是结果不可复现

从零写一个 agent 循环

去掉框架,agent 循环的骨架就是一个 while:

agent_loop.py · 去掉所有框架之后剩下的东西
def run(task, tools, max_turns=20):
messages = [{"role": "user", "content": task}]
for turn in range(max_turns):
res = client.chat.completions.create(
model=MODEL, messages=messages, tools=schemas(tools),
)
msg = res.choices[0].message
messages.append(msg)
if not msg.tool_calls: # 模型觉得不用工具了
return msg.content # ← 这里是它自己决定停
for call in msg.tool_calls: # ← 这里是它自己决定用哪个工具
out = tools[call.function.name](**json.loads(call.function.arguments))
messages.append({"role": "tool", "tool_call_id": call.id, "content": str(out)})
return "达到轮数上限" # ← 有界,不许无限转

加粗那两行就是全部的「自主性」 —— 模型选工具,模型决定停。其余全是管道。

这段代码值得自己敲一遍,因为后面五周的框架,本质上都是在这个循环外面包东西:加追踪、加会话、加多 agent 编排、加协议。看懂了这十几行,再去读任何一个框架的文档,你都能一眼认出它在替你做哪一段。

工具调用的真相

「工具调用」听起来像模型能执行代码,实际上不是。

流程是:模型输出一段结构化文本说「我想调 read_file,参数是这个」→ 你的代码去执行 → 把结果作为一条消息塞回对话里。

模型自始至终没碰过任何东西。 它只是在生成一种约定格式的文本。

这个认识有三个直接后果:

  1. 执行的安全边界全在你这边。 模型说要删文件,删不删是你的代码决定的。
  2. 工具描述写得好不好,直接决定它用得对不对。 这一条单独有一篇讲,杠杆大到超出直觉。
  3. 模型会调不存在的工具、传错参数类型。 解析层要认下这些,而不是指望它每次都守约。

上下文工程:这一周最被低估的一格

第一周还会碰到「记忆、工具、RAG」这一组。它们的共同问题是:一次请求里到底塞什么。

有一个反直觉的证据值得早点知道。Adobe 那篇 NoLiMa 把大海捞针题改造了一下 —— 让问题和答案没有字面重合,逼模型去认语义而不是抄词。结果:到 32K 长度,13 个模型里有 11 个掉到自己短上下文成绩的一半以下;Llama 3.1 70B 从 94.3% 掉到 42.7%,论文给它算出的有效长度只有 2K

注意这不是「窗口装不下」,是窗口没满、性能已经在掉 —— 这个现象有个名字,叫上下文腐烂标称窗口和有效长度是两个数,而你在读的那个大数字是前者。

我自己在这一格上栽过一次,而且是最没面子的那种栽法:我给上下文加了截断,超长就砍,并且只在控制台 warn 一句「截掉了约 3000 token」。后来要复盘一次覆盖不全的运行,我答不上来「被砍掉的那部分里有没有关键规则」—— 因为原文已经不在了。我的判断是:有损的可以是喂给模型的那一份,落盘的那份必须完整,这条现在写死在我的代码里。。

所以上下文工程的目标不是「尽量多给」,是「只给这一步需要的」。这一条在单次调用里影响不大,在 agent 循环里会被放大 —— 因为循环把自己的输出喂回去,上下文一轮轮变长。

边界与代价

能用 workflow 解决的,别上 agent。 代价不是性能,是可复现性:同一份输入两次运行走不同路径,你就没法比较两个版本,也没法做 A/B。

「从零写循环」是学习手段,不是生产建议。 那十几行没有重试、没有预算上限、没有追踪、没有并发隔离。它的价值在于让你看清框架在替你做什么 —— 然后你才有资格判断某个框架做的是不是你要的。

第一周的项目(数字分身)有个隐藏前提:它的输出没有客观对错。一个代表你回答问题的 agent,答得好不好只能靠人看。这类没有便宜验证器的场景,恰恰是最难做好的 —— 后面几周的项目会好一些,因为它们有测试、有价格、有可比对象。