Day 006:别被概念忽悠了,Agent 的核心就是个 While 循环

系列:100 天系统学习 AI Agent 开发
当前阶段:Agent 基础与环境搭建(Day 006/100)
今日目标:抛开框架名词,用原生代码拆解 Agent Loop(计划、行动、观察、再计划),跑通最小闭环。


很多人刚接触 AI Agent 时,容易被 LangChain、LangGraph、CrewAI 里各种炫酷的专有名词唬住,觉得 Agent 是某种具有“自主意识”的黑魔法。

但如果我们戴上工程的眼镜去拆解,就会发现一件极具“祛魅”色彩的事实:绝大多数 Agent,底层都在重复同一个简单到令人发指的循环——理解任务、选择动作、执行工具、读取反馈、决定是否继续。

如果今天不把这个 Loop(循环)从代码层面想透,后面做工具集成、长记忆管理和自动化评测时,你一定只是在盲目地调包和凭感觉 Debug。今天我们不贪多,只讲透这一个点,并手写一段任何框架都能用的核心逻辑。


一、 3 句人话:到底什么是 Agent Loop?

新手最容易陷入的误区,是把 Agent 等同于“一个写得很复杂的 Prompt”。我们先纠正这个认知:

  1. Agent 不是魔法,它是一个将“大模型推理”和“外部代码执行”死死绑在一起的while循环。
  2. 每次循环里,大模型只做做选择题:看眼历史记录,决定接下来是该“调用某个函数干活”,还是“已经有答案了,直接回复用户”。
  3. 所谓的“自主思考(Re-plan)”,不过是代码把工具执行后拿到的“观察结果(Observation)”塞回历史记录里,让模型再做一次选择题而已。

二、 责任边界:模型干什么?代码干什么?

千万不要把什么活全推给大模型。工程落地落地最重要的一步,就是画清“大脑”和“手脚”的边界:

模块必须由大模型负责(思考与决策)必须由代码兜底(控制与安全)
任务职责• 解析用户意图与上下文


• 决定下一步调用什么工具


• 生成正确的工具参数


• 汇总最终的人性化回答 | •死循环拦截(设置最大轮数)


• 工具执行的权限校验与沙盒隔离


• API 调用失败的重试与错误捕获


• 历史记录(Observation)的持久化与剪裁 |


三、 最小闭环与工程实践

1. 最小闭环逻辑图

把上面的逻辑抽象一下,它的流转路线其实非常干净:

用户输入 (Query) │ ▼ ┌─────────────────────────────────────────┐ │ While 循环在运行 │ │ │ │ ┌─────────────────────────────────┐ │ │ │ 1. Plan: 大模型决定下一步行动 │ │ │ └─────────────────┬───────────────┘ │ │ │ │ │ [ 是否调用工具? ] │ │ / \ │ │ 是 否 │ │ ▼ ▼ │ │ ┌──────────────┐ ┌─────────────┐ │ │ │ 2. Act: │ │ 4. Output: │ │ │ │ 代码执行工具 │ │ 输出最终结果 │ │ │ └──────┬───────┘ └─────────────┘ │ │ ▼ │ │ │ ┌──────────────┐ │ │ │ │ 3. Observe: │ │ │ │ │ 记录结果并回传 │ │ │ │ └──────┬───────┘ │ │ │ │ │ │ └──────────┼─────────────────────┼────────┘ │ (Re-plan) ▼ └──────────────► 任务完成,退出循环

2. 伪代码实现(限制最多 5 轮调用)

这不仅是一段概念演示,你可以直接把它作为自研 Agent 核心路由的雏形:

defagent_loop(user_query:str,max_steps:int=5):# 初始化对话历史,把用户意图塞进去messages=[{"role":"user","content":user_query}]step=0# 核心:由代码控制的最大轮数兜底,绝不把停止的命运完全交给 LLMwhilestep<max_steps:step+=1print(f"--- 正在执行第{step}轮思考 ---")# 1. Plan & Act决策:让模型看历史记录,决定下一步response=llm.chat(messages=messages,tools=available_tools)# 2. 停止条件判断:如果模型觉得信息够了,没有发起工具调用,直接输出ifnotresponse.has_tool_calls():print("任务完成!")returnresponse.final_content# 3. Act(执行):解析模型想调用的工具和参数tool_name=response.tool_call.name tool_args=response.tool_call.argsprint(f"-> 动作 (Act): 准备调用工具 [{tool_name}],参数:{tool_args}")# 4. 代码执行真实的函数(捕获异常,防止代码崩溃)try:observation=execute_tool(tool_name,tool_args)exceptExceptionase:observation=f"工具执行报错:{str(e)}"print(f"-> 观察 (Observe): 工具返回结果 ->{str(observation)[:50]}...")# 5. Re-plan(再计划):把把这次调用和执行的结果追加到记忆里# 下一次 while 循环时,模型就能看到“我调了工具,得到了这个结果”,从而进行下一步判断messages.append({"role":"assistant","tool_call":response.tool_call})messages.append({"role":"tool","name":tool_name,"content":str(observation)})# 如果触发了强制退出return"已达到最大执行轮数(5次),任务被迫中止,请检查任务复杂度或工具可用性。"

四、 踩坑实录:为什么max_steps绝对不能省略?

在今天写小实验的时候,我想测试一下如果不加while step < max_steps会发生什么,结果故意输入了一个具有误导性的测试用例:

反例输入:“帮我查一下‘亚特兰蒂斯南区’今天下午2点到3点的准确降雨量(精确到毫米)。”

发生了什么?

  1. 大模型极其认真,调用get_weather工具去查。
  2. 工具老实返回:Error: Location '亚特兰蒂斯南区' not found.
  3. 陷入死循环:模型开始展示它“顽强”的自主思考能力(Re-plan),猜测可能是名字拼写问题,于是依次调工具查了:“Atlantis”“亚特兰蒂斯”“亚特兰蒂斯市”、甚至把问题拆解成去查*“虚拟城市天气查询接口”*…
  4. 几秒钟内,工具被疯狂调用了十几遍,不仅瞬间烧掉了一堆 Token,还把模拟的下游 API 给触发限流了。

教训:
永远不要假设大模型“知道什么时候该放弃”。当工具报错或任务无解时,模型很容易陷入死循环死磕。工程上的强制重试上限(max_steps)和超时的机制,是你的 Agent 不会因为某个 Bug 让你破产的最后一道防火墙。


五、 今日产物:学习归档

按照我们 100 天学习计划的铁律:每天都要留下一个可以放入项目 README 或记忆库的工程结构化产物。今天的沉淀已保存至项目目录notes/day_006_learning.md

{"day":6,"topic":"Agent Loop 拆解:计划、行动、观察、再计划","core_takeaway":"Agent 本质是一个将 LLM 决策与代码执行挂钩的 while 循环。自主能力来源于不断把 Observation 塞回 Prompt 后再次生成。","code_artifact":"实现了一个带限制上限 (max_steps=5) 和异常捕获的原生 Agent Loop 伪代码。","pitfall_recorded":"无边界死循环:当工具持续返回错误或查询不存在的内容时,缺乏轮数限制会导致模型无限尝试,消耗大量 Token。","next_day_input":"既然 Loop 会把所有的 Observation 追加到 messages 里,那明天如果工具返回了一份 5000 行的 JSON,上下文窗口被撑爆了怎么办?"}

读者行动指南:今日你能怎么做?

如果你也在同步搭自己的 Agent 项目,建议你今天只做一件事:
打开你现在最常用的 Agent 框架(不管是 LangChain 还是 OpenAI SDK),找到底层的源码,尝试搜索while或者loop关键字。

看看那些年收几十万 Star 的框架,在剥离了所有包装类后,是怎么小心翼翼地维系这个最简单的“思考-执行-观察”循环的。看完你就会懂,真正的优雅,往往隐藏在最朴素的逻辑里。