Skip to content

你从第 01 章进入,还缺 00 前置。

自测代码证据锁定

第 1 章:什么是 Agent——从回答器到受控执行系统

← 上一章:Agent 全景 · 下一章:百行 Agent Loop

开场:一个会循环的程序,是 Agent 吗?

想象你要给客户办一张「退款工单」。你面前坐着四类「客服」,能力一级比一级高:

  • A:手边一本 FAQ 手册,只回答手册里写好的问题;
  • B:很会说话,能接住任何问法,但没有任何权限,只能口头答复;
  • C:在 B 的基础上,还记得上次接待过你、你说过什么;
  • D:能真的打开系统操作,但每一次操作都要先经过主管批准。

这四类客服,只有 D 才真正「办成了事」。可麻烦在于:光看对话,你很难分清 B、C、D——它们都「说得像那么回事」。很多自称 Agent 的程序,其实只是 B 或 C:能聊、能记住,但该做的权限、状态、停止条件一个都没有。

这一章带你看清这四档能力差在哪,以及把一段「会说话」的程序,变成一套「受控执行」的系统,到底多了哪些东西。

本章结束时,你能回答:「一个程序有 while True,它就是 Agent 吗?」——答案是带条件的,不是一句是或不是。顺带先认识一下这个词:while True 表示「这段代码永远重复执行」——只要没人主动打断,它就一直循环下去。后面你会看到,光靠这一点循环远远不够。

四位客服里,谁真的对外界产生了影响?

  • A:照着 FAQ 手册念出回答
  • B:很会说话,但没有任何权限
  • C:记得你上次说过什么,但只回复口头文字
  • D:每次操作都经主管批准后,真的改了系统里的数据

先看最便宜的一档:写死的规则

最低的一档不碰任何模型。把常见问题写死成规则,命中就返回固定答案:

python
def answer(question: str) -> str:
    # 如果用户的问题里带「营业时间」……
    if "营业时间" in question:
        # ……就返回写好的固定营业时间
        return "周一至周五 09:00–18:00"
    # 其余问法一律转人工
    return "请转人工客服"

它稳定得无可挑剔:同样的输入永远同样的输出,想测就能测。代价是只能覆盖写进规则的那些问法——把「营业时间」换个说法「你们几点开门?」,它就不会了。

记住这一档,不是为了让你用它,而是立一个基线:之后每一档都多出一些能力,也多出一些失败面。 后面你会反复用到这张对照表:

客服类型新能力新增的麻烦
A 写死规则稳定、可测换种问法就漏
B 很会说话接住任何问法说得像真的,但可能记错、不重复
C 记得前文能延续话题记录会膨胀、旧的会污染新的
D 真能执行办成事越权、副作用、停不下来

换成一个很会说话的:但每次都是陌生人

规则搞不定了,于是换上模型。它听得懂任何问法,能自然作答。对应的代码只有几行:

python
reply = await model.complete(
    messages=[{"role": "user", "content": question}],
    tools=[],
)
print(reply.content)

逐行看:await 是「等模型回答完」——模型生成文字要花时间,代码先停下来等结果。messages 是你递给模型的那份「对话记录」,里面一条条消息(谁说的、说了什么);这里只放了你当前这一句话,用 {"role": "user", ...} 标成「用户说的」。tools=[] 表示这一档不提供任何动作。print(reply.content) 把模型返回的文字打印出来。

但这里有个特别容易误解的地方:它每次接待都像第一次见你。 你上一句说「我叫小梁」,下一句问「我叫什么」,它答不上来——不是笨,是这一次对话里,它手上根本没有「小梁」这条信息。

换句话说,模型的能力是一把好用的嘴,但「记忆」不归它所有。它对任何一次对话来说都是「初来乍到」,除非你把上次的对话一起递给它——这正是下一档做的事。

为什么 B 类客服「不记得你说过自己叫小梁」?

  • 它记性天生不好
  • 这一次对话里,它手上没有「小梁」这条信息
  • 它的权限不够
  • 它需要先上网查一下

把上次的接待记录带回来

失忆的修法不神秘:把之前的对话一起带过去。代码上就是多维护一个消息列表,每轮把新问答追加进去,再整份递回给模型:

python
# 把用户这句话追加进对话记录
messages.append({"role": "user", "content": question})
# 把整份记录(含历史)一起发给模型
reply = await model.complete(messages=messages, tools=[])
# 把模型的回答也存进记录,下一轮接着带
messages.append({"role": "assistant", "content": reply.content})

{"role": "assistant", ...} 就是「模型说的」那条消息——和用户说的消息并排存着,合起来就是完整的对话记录。

这里要特别提醒你一个常见的错觉:「记住」不等于「模型有记忆」,而是「你再次提供了记录」。 那份记录每次都是你亲手重新组装的。记录没保存、进程一关、或者记录太长被压缩丢了关键几轮,它照样忘得干干净净。

很多 Agent 代码看起来一直在循环(while True——还记得吧,永远重复执行),但每一轮手上还是只有当下这一句话——循环不等于记忆,差的就是那本记录。判断一个程序是不是真记得,就看它在每一轮是否真的把上次的对话带上了。

一个程序有 while True 循环却仍然「失忆」,最可能的根因是?

  • 循环跑得还不够多
  • 模型每次都被重置了
  • 每一轮都只发了当前这句话,历史没有被带上
  • 系统提示里忘了写「记住用户」的指令

真能办成事,但要有人批

到这里,程序终于能执行外部动作了——搜索、写文件、开工单。但「能执行」和「有权执行」是两回事。

你可以把它想成 D 类客服:他看得见系统里所有操作按钮,但每一次按下去之前,都要先经主管批准。对应到代码里:

  • 模型负责提议:「我想按下这个按钮(调用某个工具)」;
  • 代码里的权限检查负责批准或拒绝;
  • 批准之后,工具才真的执行,结果再放回对话继续。

把这套关系组装起来,长这样:

python
runner = AgentRunner(
    model=model,                                          # 模型:负责「提议」
    tools=registry,                                       # 工具:可用的动作清单
    policy=CapabilityPolicy(allowed_tools=frozenset({"search_kb"})),  # 批准/拒绝由它把关
    max_iterations=8,                                     # 最多循环 8 轮,防止停不下来
)

注意 policy 这一行:它写明了模型只能建议 search_kb 这一个工具。模型说「我要调用 delete_all_files」?权限检查直接拒绝——模型可以提议任何它看到的工具,但能不能落地,由代码把关。 max_iterations=8 回答的是「什么时候停下来」:默认最多转 8 轮,免得它永远转下去。

这套把「提议、批准、执行」分开的控制机制,就是常说的 Harness。它把一次次孤立的模型调用,编排成一个受控的循环:什么时候继续、什么时候停下来、每一步发生了什么、出错了谁负责,都由它说了算。

判断一个程序是不是真 Agent,除了「能执行」,还要看它是否可观察。界面上显示的状态,应该来自运行记录下来的真实事件,而不是模型自己说的「正在查询」——否则你分不清它真在干活,还是在表演。一次真实运行的记录长这样:

text
turn.started
model.completed(iteration=1, tool_calls=["search_kb"])
tool.started(name="search_kb")
tool.completed(result=[...])
model.completed(iteration=2, tool_calls=[])
turn.completed(iterations=2)

把四档连起来看,就是一个完整的分流决策:

这张图也是一条产品原则:不需要 Agent 的任务,别强行 Agent 化。 能用普通函数稳定解决的事,就把模型留在需要语言理解和开放决策的地方。

D 类客服「每次操作都经主管批准」,对应代码里谁说了算?

  • 模型自己,因为它最懂用户
  • 系统提示里写的「按需创建工单」
  • 代码里的权限检查
  • 用户的自然语言要求

动手:让两个失败发生,再用一个测试拦住

理论和手感之间隔着一道墙,亲手撞一次才会留下印象。下面两段代码都是故意写错的,请原样跑一遍。

失败一:有循环,没记忆。

python
import asyncio

async def no_memory_agent():
    while True:   # 永远重复这一段
        question = input("你问:")   # 停下等用户输入一句话
        reply = await model.complete(
            # 每次都只发当前这一句,历史根本没带上
            messages=[{"role": "user", "content": question}],
            tools=[],   # 不提供工具
        )
        print(reply.content)   # 打印回答,然后回到 while True

asyncio.run(no_memory_agent())

先输入「我最喜欢蓝色」,再输入「我最喜欢什么颜色」。它答不上来——每一轮它都只看到当前这句,之前的对话一次都没带。再把上面「把上次的接待记录带回来」那版代码换进来跑同样的输入,它能答上来。差异来自上下文,不是模型换了。

失败二:把「系统提示」当成安全边界。

python
# 失败实验:只靠提示语,拦不住危险工具
system_prompt = "不要调用危险工具!"   # 提示语是「劝」,不是「锁」
tools.register("delete_all_files", delete_all_files)   # 危险工具还是被注册、被模型看见了

# 模型只要返回「调用 delete_all_files」的指令……
# ……在没有权限检查的情况下,工具就会真的执行——提示语拦不住

跑完后用现成测试验证权限检查真的能拦住:

bash
cd "$(git rev-parse --show-toplevel)"
uv run --python 3.12 --extra dev pytest \
  mini-emperor/backend/tests/test_agent.py::test_capability_policy_blocks_undeclared_tool \
  -v

如果它不过,按这个顺序排查是哪一层的问题:

  • 模型选错工具 → 工具描述、上下文或模型的问题;
  • 参数不合法 → 参数校验的问题;
  • 不该执行却执行 → 权限检查的问题
  • 执行了却没被回答用上 → 结果回填的问题。

门禁·代码:跑通上面这条测试,把输出保存为证据。

本章小结

这一章没有给你一个「是 / 不是」的判定器,而是给了你一条分档的尺子。把它连起来看:

Agent 不是一步到位的,而是四档能力逐级叠加的结果。 写死规则 → 换模型会说话 → 带上记录有记忆 → 能执行动作且受控。每一档都新增一种能力,也新增一类失败面。

「会说话」只是最表层的标签。 很多程序停在「很会说话」(B)或「记得你说过什么」(C),就自称 Agent。真正的分界线在 D:能不能执行外部动作,并且执行是受控的。

把 D 档拆开,就是三个问题:

  1. 谁批准? 模型提议调用工具,代码里的权限检查决定是否真的执行;
  2. 怎么知道? 状态必须来自运行记录的事件,而不是模型自己说的话;
  3. 什么时候停? 要有明确的停止条件——轮数上限、完成证据、转人工。

一句话记住这一章:

模型负责提议,代码负责批准与执行,环境反馈负责证明。会说话、记得住、能执行、受控地停——这才是 Agent。

如果上面这些话你自己也能说得出来(不看资料),就说明这一章的知识已经连成一片,不是散落的碎片。

复述与证据

合上资料,把这四档客服从头讲一遍:每上一档多出什么能力、多了什么麻烦。如果你能一句话概括 D 类——模型提议、代码批准、环境反馈——你就掌握了本章的核心。

再追问两个问题:

  1. 为什么「有 while True」不等于「有记忆」?
  2. 判断 Agent 是否完成任务,要凭外部证据,而不是它自己说的话——为什么?

门禁·证据:提交一条真实的运行记录(turn.started → model.completed → tool.started → tool.completed → turn.completed),并标出每一步是谁产生的(模型 / 代码 / 环境)。

24 小时后:不看资料,画出「模型提议 → 代码批准 → 工具执行 → 结果回填」的往返过程。

有兴趣可以继续看

以下资料按优先级排。至少完成一项 P0,并保存完成证据。

视频P010 分钟 · 视频 01 [00:00:15–00:02:54]
预计
10 分钟
位置
视频 01 [00:00:15–00:02:54]
阅读任务
暂停在四要素和能力边界,分别写出一个工程对应物。
完成证据
完成四档能力对照表,每档至少写一个失败面。
教学仓库P045 分钟 · Step 01–04 代码与同名 doc
预计
45 分钟
位置
Step 01–04 代码与同名 doc
阅读任务
先运行「只有循环」那一版的失忆,再运行「带上历史」那一版;最后比较系统提示参数。
完成证据
保存两次消息列表和一段差分说明。
书与实验P050 分钟 · ai-agent-book 第 1 章、chapter1/context
预计
50 分钟
位置
ai-agent-book 第 1 章、chapter1/context
阅读任务
读公式、ReAct、Harness,并做 History 消融实验。
完成证据
写出失效的是模型能力,还是可见信息。
论文P050 分钟 · ReAct Figure 1
预计
50 分钟
位置
ReAct Figure 1
阅读任务
区分 Reasoning、Action、Observation;映射到运行事件。
完成证据
提交一条不暴露私有推理的公开运行轨迹。
官方文档P140 分钟 · DeepSeek Create Chat Completion
预计
40 分钟
位置
DeepSeek Create Chat Completion
阅读任务
只读 messages、tools、tool_calls 与流式事件字段;核对当前模型名。
完成证据
写出供应商消息格式与课程 ModelReply 的适配表。

本文提到的代码与出处(有兴趣逐条核对时用):

← 上一章:Agent 全景 · 下一章:百行 Agent Loop

Markdown 是唯一内容源,HTML 由构建流程生成。