Deepseek Agent Harness教程(一) | 什么是agent

从“只会聊天”到“真能干活”:一文讲透 AI Agent 到底是什么
别人说“我写了个 Agent”,他到底写了什么?答案会让很多人失望——他写的那部分,基本没有 AI。
一、先搞清楚:大模型和 Agent,根本不是一回事
很多人把“大模型”和“Agent”混着用,这是第一个要纠正的误解。
大语言模型(LLM) 本质上就是一个吃文字、吐文字的函数。你给它一段话,它还你一段话。它不能读文件、不能上网、不能执行命令、也记不住你五分钟前说了什么。
那为什么你在 ChatGPT 里跟它聊十轮,它好像都记得?
真相是:你的聊天软件在第十轮时,把前面九轮的全部对话重新发了一遍。 模型那边根本不知道有“第十轮”这回事,它只是收到了一大段文字(恰好包含之前所有的聊天记录),然后接着写下去。
模型像一个每天早上彻底失忆的专家。你要他接着昨天的工作干,唯一的办法是每天早上把昨天的完整工作日志重新念一遍给他听。
用公式看清楚“没有记忆”到底是什么意思
设大语言模型为一个函数 f,它只做一件事:接收一段文字,返回一段文字。当你和它进行第 n 轮对话时,实际发生的事情是:
r n = f ( S , m 1 , r 1 , m 2 , r 2 , … , m n − 1 , r n − 1 , m n ) r_n = f(S, m_1, r_1, m_2, r_2, \ldots, m_{n-1}, r_{n-1}, m_n) rn=f(S,m1,r1,m2,r2,…,mn−1,rn−1,mn)
| 符号 | 含义 | 通俗解释 |
|---|---|---|
f |
大模型本身 | 那台只会“接一段、续一段”的机器 |
S |
系统提示词 | 每次都要重发的“开场白” |
m_k |
第 k 轮用户说的话 |
你打的字 |
r_k |
第 k 轮模型的回复 |
模型上一次吐出来的文字 |
n |
轮数 | 现在聊到第几个来回 |
关键点解读:
-
右边括号里有什么? 第
n轮的输入,包含了前面所有轮的输入和输出。这就是“没有记忆”的精确写法——模型并不是“想起来”你之前说了什么,而是你的程序把之前的全部对话历史又重新塞进了这次请求里。 -
模型自己存了什么? 什么都不存。它只是收到了括号里的那一长串文字,然后根据这段文字续写出
r_n。 -
所谓“记忆”,是谁在维护? 括号里的那一长串东西——也就是全部的对话历史——是你的程序负责保存和重发的,不是模型。
一个常见的误解
很多人以为“模型能记住 128K 上下文”的意思是“模型自己存了 128K 的记忆”。不是。 那个数字是单次输入的长度上限——是每次你能往括号里塞多少东西的上限。上限之内的东西,仍然要你每次原样重发。
这个公式的后果
假设每一轮对话新增的内容量差不多是常数 c,因为每轮都要把之前的全部重发一遍,那么聊到第 n 轮为止,你累计发送的总量是:
T ( n ) = c ⋅ n ( n + 1 ) 2 T(n) = c \cdot \frac{n(n+1)}{2} T(n)=c⋅2n(n+1)
关键是 n 2 n^2 n2:轮数翻一倍,花的钱和 token 量变四倍。 10 轮对话的成本是 1 轮的 55 倍,而不是 10 倍。
这就是为什么 Agent 跑长任务时账单会突然失控,也是为什么所有成熟的 Agent 框架都有一整章专门讲“怎么压缩历史”——因为不压,程序就直接报错了。
那 Agent 是什么?
Agent(智能体) 是一个能自己循环调用模型、并且能真的动手干活(读写文件、执行命令、访问网络)的程序。
一句话总结:模型是“大脑”,Agent 是“大脑 + 手脚 + 循环”。模型只会说,Agent 真的干。
二、Agent 是怎么工作的?二十行代码说清楚
把 Agent 拆到最简,它就是一个循环,核心就四步:
- 把整段对话发给模型
- 如果模型说要调用工具,就执行它
- 把执行结果塞回对话
- 重复,直到模型不再要工具
用代码写出来,核心就二十行:
const TOOLS = {
read: (args) => fs.readFileSync(args.path, 'utf8'),
write: (args) => { fs.writeFileSync(args.path, args.text); return 'ok' },
}
const messages = [{ role: 'user', text: '把 report.txt 的第一行念给我听' }]
while (true) {
// ① 整段对话重发——因为模型没有记忆
const reply = await callModel(messages, schemasOf(TOOLS))
messages.push(reply)
// ② 模型不再要工具了,收工
if (!reply.toolCalls || reply.toolCalls.length === 0) break
// ③ 逐个执行——这是唯一真正改变世界的地方
for (const call of reply.toolCalls) {
let out, isError = false
try {
out = String(await TOOLS[call.name](JSON.parse(call.arguments)))
} catch (e) {
out = String(e); isError = true
}
// ④ 结果变成消息接回去
messages.push({ role: 'tool', toolCallId: call.id, text: out, isError })
}
}
这段代码真的能跑,而且它真的是一个 Agent——不是玩具,是同一个东西的最小形态。
三、工具调用:模型“动手”的真相
很多人第一次听到“模型可以调用工具”,脑子里浮现的是:模型内部长了一双手,能伸出来碰文件。这是错的,而且错得很重要。
真实的机制是这样的:
- 你在发请求时,附上一张工具清单:每个工具叫什么、干什么用、需要什么参数
- 模型读完,如果觉得该动手,就吐出一段结构化文字,大意是“我要调用
read_file,参数是{path: 'report.txt'}” - 你的程序解析这段文字,真的去打开文件、读出来——这一步纯粹是普通编程,一点 AI 都没有
- 你的程序把读到的内容包成新消息,接在对话后面,整段再发一次给模型
模型只是“说”了要干什么,真正动手的是你的程序。
所以,工具能干什么,完全取决于你的程序允许它干什么。模型说“删除整个磁盘”和说“今天天气不错”,在传输层面是完全一样的东西——都是一段文字。会不会真删,取决于你的代码里有没有写那段删除逻辑、有没有做权限控制。
这就是为什么 Agent 的安全和审批机制,从头到尾没有 AI 参与——它全是普通程序的活儿。
四、状态机:为什么 Agent 不会“乱成一锅粥”
上一节我们看了二十行的最小 Agent,它在一个完美的、没人打断、不会出错的世界里能跑。但真实世界不是这样——用户会中途按取消,网络会断,工具会超时,依赖的服务可能还没启动。
如果你用一堆布尔标记来控制这一切,很快就会陷入混乱。
布尔标记的陷阱:16 种组合里只有 5 种合法
假设你要管理一个工具调用的状态,直觉上你可能会写:
let isLoading = false // 正在执行中?
let isDone = false // 已经完成了?
let hasError = false // 出错了?
let isCancelled = false // 被用户取消了?
这四个布尔值有 2 4 = 16 2^4 = 16 24=16 种组合。但其中合法的可能只有这几种:
| isLoading | isDone | hasError | isCancelled | 合法? |
|---|---|---|---|---|
| false | false | false | false | ✅ 初始状态 |
| true | false | false | false | ✅ 执行中 |
| false | true | false | false | ✅ 完成 |
| false | false | true | false | ✅ 出错 |
| false | false | false | true | ✅ 已取消 |
| true | false | true | false | ❌ 既在执行又出错 |
| true | false | false | true | ❌ 既在执行又被取消 |
| false | true | false | true | ❌ 既完成又被取消 |
| … 还有 8 种 | … | … | … | ❌ 全是非法状态 |
所有那种“怎么会同时又在加载又已经出错”的诡异 Bug,都出在这 11 种不该出现但代码没拦住的状态里。
更麻烦的是:Bug 复现不了。因为状态组合取决于网络延迟、磁盘速度、用户点击的精确时机——这些完全不受控的因素。
状态机的解法:从 16 种压到 5 种
状态机的定义简单到有点可疑:一样东西在任何时刻处于有限个状态中的某一个,并且只能沿着规定好的箭头从一个状态转到另一个。
把上面的工具调用画成状态机就是:
┌──────────────────────────────────────┐
│ ▼
┌─────────┐ 用户点击 ┌─────────┐ 完成 ┌─────────┐
│ 空闲 │ ──────────► │ 执行中 │ ──────► │ 已完成 │
└─────────┘ └─────────┘ └─────────┘
│ │ 超时 │
│ 取消 ▼ │
│ ┌─────────┐ │
└────────────► │ 已取消 │ ◄─────────────┘
└─────────┘
▲
│ 出错
┌─────────┐
│ 已失败 │
└─────────┘
合法的转移只有:
- 空闲 → 执行中(用户点击)
- 执行中 → 已完成(顺利结束)
- 执行中 → 已失败(报错)
- 执行中 → 已取消(用户中途取消)
- 空闲 → 已取消(用户还没开始就取消)
非法转移在类型层面就表达不出来: 代码里根本写不出“从已完成跳到执行中”或“同时处于执行中和已失败”。不是靠运行时检查,而是在设计阶段就被排除掉了。
真实案例:DeepSeek Harness 的 Fiber 六状态
DeepSeek Harness 底层用了一个叫 Cordis 的框架来管理所有插件。每个已加载的插件实例叫 Fiber,它的状态在源码里明明白白写着:
export const enum FiberState {
PENDING, // 在等依赖,还没开始
LOADING, // 正在启动
ACTIVE, // 跑着呢
FAILED, // 起崩了
DISPOSED, // 被拿掉了,回不来了
UNLOADING, // 正在拆
}
PENDING 和 FAILED 是两件完全不同的事:
- PENDING:我还没开始,因为条件没满足(比如依赖的服务还没启动),条件满足了我就能自动起来
- FAILED:我试过了,我起不来,需要人去修
如果只用一个布尔 isBroken,这两件事就混在一起——你永远搞不清该去安装一个缺失的依赖,还是该去修一段崩溃的代码。
状态之间的转移也不是任意的:
PENDING → LOADING → ACTIVE
↘ FAILED
UNLOADING → DISPOSED
- 从
PENDING只能去LOADING(依赖就绪)或UNLOADING(被强制卸载),不能直接去ACTIVE - 从
LOADING可能去ACTIVE(启动成功),也可能去FAILED(启动过程崩了) - 从
ACTIVE只能去UNLOADING(正常卸载),不能直接变FAILED - 从
FAILED或DISPOSED没有出路——死透了,再也回不来
状态转移的箭头,就是你对系统行为的全部承诺。
红绿灯的类比,以及它在哪里失效
打个比方: 状态机就像红绿灯。红、黄、绿三个状态,箭头是红→绿→黄→红,不能红直接跳黄,也不能同时亮两个灯。
类比失效处(也是理解的关键):
- 红绿灯是时间驱动的——到点就变。程序里的状态转移是事件驱动的——依赖就绪了才从 PENDING 走到 LOADING,可能是 1 毫秒后,也可能永远不会。
- 红绿灯只有一条环路,Fiber 的箭头有分叉——LOADING 之后可能去 ACTIVE,也可能去 FAILED,这是两种完全不同的结果。
- 红绿灯没有“出错”状态——灯丝烧了它不会自己知道。程序里状态机必须把“执行失败”作为一等公民状态,而不是“啥都不亮”这种未定义情况。
Agent 里哪些地方用了状态机?
把你那二十行的最小 Agent 放到真实世界里,每个环节都需要状态机:
| 环节 | 状态机管什么 |
|---|---|
| 工具调用 | 空闲 → 执行中 → 已完成 / 已失败 / 已取消 |
| 会话生命周期 | 初始化 → 就绪 → 运行中 → 压缩中 → 结束 |
| 插件加载 | PENDING → LOADING → ACTIVE / FAILED |
| 用户审批 | 等待审批 → 已批准 → 已执行 / 已拒绝 |
| 重试策略 | 首次尝试 → 重试中(带计数)→ 成功 / 失败 |
Agent 循环本身也是一个状态机:
┌─────────────────────────────────────┐
│ │
▼ │
┌──────────┐ 模型回复含 ┌──────────┐ 执行完成 ┌──────────┐
│ 等待模型 │ ────────────► │ 执行工具 │ ─────────► │ 检查结果 │
└──────────┘ 工具调用 └──────────┘ └──────────┘
│ │ 出错 │
│ 模型回复纯文字 ▼ │ 还要继续
▼ ┌──────────┐ │
┌──────────┐ │ 错误处理 │ ◄─────────────────┘
│ 结束 │ └──────────┘
└──────────┘
有了状态机,你才能明确回答三个问题:
- 当前在干什么?(当前状态)
- 接下来可能发生什么?(允许的转移)
- 发生意外时怎么办?(错误状态和转移)
没有状态机,你的 Agent 就是一个“不知道自己在哪一步、也不知道下一步该去哪”的混乱程序——而那些“偶尔复现不了”的 Bug,就是它混乱的证明。
最后,回答开头那个问题
别人说“我写了个 Agent”,他到底写了什么?
他写的是一个循环:
- 把对话塞给模型
- 解析模型说要干什么
- 真的去干
- 把结果塞回去
- 再问模型下一步
模型只负责“说”,剩下全是普通程序的活儿——而恰恰是这些普通程序的活儿,撑起了像 DeepSeek Harness 这样的框架整整 21.2 万行代码。
你那二十行的最小 Agent 能跑,但它在真实世界里会遇到:对话太长塞不下、工具执行超时、并发冲突、权限审批、断点续跑、回滚重试……这些问题一个都不需要 AI,但一个都躲不掉。
Agent 最难的部分,从来不在 AI。
更多推荐



所有评论(0)