1. 先把概念讲清楚:Loop Engineering 到底在解决什么问题
1.1 Agent 不是“一次问答”,而是“一个闭环”
今年做 AI Agent 相关的项目,有一个词几乎绕不开——Loop Engineering。我第一次听到这个词时,正在调试一个翻来覆去调用同一个搜索工具、就是不往下走的 Agent,那一刻我才真正意识到:把大模型接进一个循环里,比让大模型输出一段好看的 JSON 难多了。
简单来说,Loop Engineering 就是把“思考—行动—观察—再思考”做成一个显式的循环:大模型先判断自己要做什么,调用一个工具,把工具返回的结果当作新输入,继续推理,直到达成目标。过去我们习惯把 LLM 当成一个“搜索框”,问一句答一句;但真实任务里,比如整理一份竞品分析、抓取一个网站并提炼要点,根本不是一次对话能完成的,它需要连续的决策和多个工具的协作。
这就像开导航去一个陌生地方,不是出发前看一次地图就能到,而是每个路口都要看一眼当前路况、重新规划路线,再开一段,再看一次,直到抵达终点。Loop Engineering 就是把这种“边走边看边修正”的过程显式地写进代码里,让模型不再是一次性推理,而是在闭环里持续迭代。
那么,为什么这个方向在最近一段时间突然被大家反复提起?原因不难找:一是模型本身的推理能力够用了,二是工具调用的协议成熟了,三是大家发现,光靠 Prompt 很难约束长期任务的执行过程,必须在代码层面对“循环”做工程化管理。可以说,Loop Engineering 的本质,是把 Agent 的执行过程从“黑盒”变成“结构化的可控制流程”。
1.2 为什么这个思路“现在”才被集中讨论
很多人误以为 Loop Engineering 是个新概念,其实“Agent 循环”早在 ReAct、Toolformer 那批论文里就有了,只是当时的模型工具调用能力太弱,循环跑两三轮就开始胡说八道,工程上很难落地。真正让这个方向爆发的是两个变化。
一个是模型端:现在的模型在 function calling 上越来越稳,给定工具文档和参数结构,它基本能正确生成调用请求,也能理解工具返回的 JSON。另一个是框架端:OpenAI 推出 tool calls 结构以后,模型输出已经不再只是“一段文字”,而是一份结构化的动作指令,代码里可以做分支判断——是返回最终答案,还是继续执行工具。这样的协议层支持,让“循环”变成一个标准的工程模式,而不是 hack。
还有一个容易被忽略的原因:应用场景倒逼。RAG 解决的是“知识不足”的问题,但很多真实任务根本不是“缺知识”,而是“缺流程”。比如让 Agent 做一份竞品分析,它要先去搜索品牌信息,看完结果决定补搜某个维度,再整理对比表格,再写结论——这一步一步的行为序列没法靠一次生成搞定。当越来越多团队把 Agent 往真实业务里推的时候,循环能力就直接决定了系统能不能用。
所以你会看到 LangGraph、AutoGen、CrewAI 这些框架,核心卖点全是“图结构”“多轮状态流转”“可暂停可恢复”,说白了都是在帮开发者管理循环。Loop Engineering 不是某个人的发明,而是 Agent 工程化过程中绕不开的基础设施。
1.3 和 Prompt Engineering 的分工,别混淆
我经常看到有人把所有问题都丢给 Prompt,结果系统 prompt 写了两千字,模型还是崩。这里需要明确一个分工:Prompt Engineering 负责的是“让模型理解要做什么”,而 Loop Engineering 负责的是“让系统确保它真的做完了”。
换句话说,Prompt 是给模型看的说明书,Loop 是给代码看的执行框架。模型说“我要调用 search”,靠的是 Prompt 和工具定义;但模型调用了五次 search 仍然没有产出,这个问题只能靠循环层来解决——比如轮数上限、重复行为检测、中间结果评估。
举个我踩过的例子:早期我做一个客服工单自动分类的 Agent,系统 prompt 里写了“如果信息不足,继续追问”,结果模型每次都在兜圈子,反复问同一个问题,因为它在文本层面不知道自己已经问过了。后来我在循环层加了历史记录去重判断,发现同一问题出现三次就直接中断转入人工流程,问题立刻缓解。这就是典型的两层分工:Prompt 负责表达意图,Loop 负责兜底控制。
搞清楚这个边界之后,你再去看那些 Loop Engineering 的项目,思路会清晰很多:它不是在和 Prompt 抢工作,而是在补 Prompt 够不着的那部分系统级控制。
2. 一个健壮循环的四个核心组成部分
2.1 状态管理:消息历史与状态机
整个循环最核心的数据结构就是消息历史。不是简单的列表,而是需要严格维护的上下文上下文:系统消息、用户任务、模型决策文本、工具调用请求、工具返回结果。每一步都必须把这个历史完整地拼好,再递给模型。
我见过很多新手翻车,都是因为历史维护不完整。最常见的错误是:工具调用结果拿出来之后,没有把它的关联 id 和 tool_call_id 对应好,模型下一步就不知道这个结果是谁返回的。在 OpenAI 的协议里,tool reply 必须回填 tool_call_id,否则校验直接报错。这还不是最坑的,更隐蔽的是模型在一步里发起了多个工具调用,代码只处理了第一个,剩下的被静默丢弃——历史就缺了一块,后续推理必然乱套。
好消息是,状态管理不需要一上来就上复杂的状态机库,一个字典加几个字段就够了。我自己在手工实现阶段,习惯用一个 Session 类来存当前状态:
class Session: def __init__(self, task: str, max_steps: int = 15): self.task = task self.history = [] self.steps = 0 self.max_steps = max_steps self.seen_tool_calls = set() def append(self, message: dict): self.history.append(message) self.steps += 1 def is_exhausted(self) -> bool: return self.steps >= self.max_steps这个类看起来简单,但字段设计都是有讲究的:task 记录最原始的目标,防止模型后面跑偏了没人知道;seen_tool_calls 用来做重复检测;max_steps 是硬安全阀。等循环逻辑复杂到需要分支、暂停、恢复的时候,再迁移到状态机框架也不迟。
状态机思维是另一个关键。每轮循环其实对应一个状态迁移:初始状态 → 工具决策状态 → 工具执行状态 → 观察状态 → 再决策状态。把这一步显式写出来,你能在代码里看得非常清楚,一旦逻辑出问题,直接定位到具体环节。老实说,大多数简单 Agent 用 if-else 就够了,但你必须心里有一张状态迁移图。
2.2 工具层:可调用、可观测、可中断
工具层是循环的另一半。很多教程只教你定义几个函数然后丢给模型,但工程化的工具层需要三个能力:可调用、可观测、可中断。
可调用是基础——工具的签名要清晰,参数结构要稳定,返回结果最好统一成 JSON,这样循环层才能统一处理。我习惯所有工具都返回同一个包一层的结果:
def safe_call(tool_name: str, args: dict): try: result = TOOL_REGISTRY[tool_name](**args) return {"ok": True, "result": result} except Exception as exc: return {"ok": False, "error": str(exc)}可观测的意思是,工具的每次调用都要留下日志:谁调用的、参数是什么、耗时多少、返回什么。这一点将在下一个项目中体现得淋漓尽致——你会需要这些日志去还原模型那一整套“脑回路”。没有日志,你可能只知道模型在死循环,却不知道它在循环里看到了什么。
可中断更重要。不是每个工具调用都必须在循环里跑完,一个搜索 API 可能卡了 30 秒,一个爬虫可能永远拿不到结果。我在实践里会给每个工具调用加超时:
import signal class TimeoutError(Exception): pass def timeout_handler(signum, frame): raise TimeoutError("tool call timeout") def call_with_timeout(func, timeout_seconds=10): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout_seconds) try: return func() finally: signal.alarm(0)超时后返回的错误信息本身也算一种观察结果,喂给模型,让它自己决定是重试还是换方案。这一步看起来台阶低,却解决了我遇到过的很多线上问题:搜索 API 偶尔超时、第三方接口抽风、被反爬限制……如果没有超时中断,循环会卡在那里白白烧钱。
2.3 终止条件与安全阀:防止模型“永远不结束”
想让循环正确地结束,远比想象中难。模型经常出现两种情况:任务明明完成了还在继续调用工具,或者任务没完成就提前说“搞定”。所以终止条件不能只靠模型自觉,必须从多个维度同时设防。
第一个维度是显式完成信号。我通常会告诉模型:如果任务完成,回复的内容里必须包含 END。代码在每轮循环检查模型输出里有没有这个标记。它不能作为唯一判据,因为模型有时候会忘掉,但它是最直接的信号。
第二个维度是轮数上限。这是最硬的约束,也是无论如何都要有的安全阀。15 轮也好、30 轮也好,一旦超过就强制截断,宁可任务没完成也不让它无限跑下去。成本失控的 Agent 事故,绝大多数就是没有设置这个上限。
第三个维度是重复行为检测。模型会反复用相同的参数调用同一个工具,这是无限循环最常见的标志。我实现的时候很简单,每个工具调用都生成一个签名“工具名 + 参数哈希”,放进集合里,如果连续出现三次相同签名,就判定为陷入重复,主动中断并把这个判断结果告诉模型。
第四个维度我最近才加上,是“低置信度早停”。如果模型连续几轮都在说“我再确认一下”,但又没有新的进展,我就把它导到人工处理流程。这个有点玄学,但实际项目里真会遇到模型自我怀疑式的循环,轮数没超,工具换了,就是没进展。这种问题靠代码判断比较困难,我更建议配合日志去人工 review,别指望自动化全包。
2.4 错误反馈与自我纠错:把失败变成信息
循环 Agent 和普通脚本最大的区别是:它能“看见”自己的失败并调整。这个能力完全建立在错误反馈机制上。工具返回了错误,不能只打印在控制台,必须作为工具结果的一部分,完整地塞回消息历史里,让模型在下一次推理时看到。
我做一个爬虫任务的时候,模型调用爬取工具,页面结构变了,返回的数据是空的。如果没有错误反馈,模型会继续以为自己成功了,然后基于空数据写报告;但我在返回里加了一段“数据为空,可能是页面结构变化,请尝试备用接口”,模型立刻换了一条路线,第二次就拿到了数据。
具体实现上,工具返回的 JSON 里我会包含三个字段:ok 表示成功与否,result 是真正的数据,debug 是给模型看的额外提示。这比只返回一行报错更有用,因为 debug 字段是我根据该工具的业务背景写的诊断信息。
自我纠错的另一层是“反思”。如果循环里检测到重复行为或者连续两次同样的错误,我会往历史里插入一条虚拟助手消息:“你刚才连续两次调用了同一个工具但结果异常,请停下来评估当前策略是否有效。”这种显式的反思指令,比让模型自己凭空反思有效得多。因为模型的注意力分配有限,你不主动提醒它,它很可能忽略掉那些矛盾的历史记录。
3. 保姆级项目实战:从零搭一个“调研报告生成 Agent”
3.1 场景定义与需求拆解
理论讲完,上一段真实代码。我最近做了一个小项目:调研报告生成 Agent。输入一个主题,Agent 自动完成“搜索资料 → 筛选有效信息 → 补充缺口 → 整理成结构化报告”的完整流程。
为什么选这个场景?因为它天然需要循环。第一步搜索结果往往是不够的,模型需要根据已找到的信息,决定下一步搜什么;信息收集到一定程度,还要判断有没有缺口,决定是继续搜还是开始写。整个过程是典型的“决策—行动—观察”闭环,非常适合用来演示 Loop Engineering。
技术栈选择上,我没有直接用 LangGraph,而是手写了不到两百行的 Python 循环。原因很简单:手写循环能让你把状态流转、终止条件、错误反馈这些细节吃透,之后再迁移到框架,你会更容易理解框架设计者为什么那么做。API 我用的 OpenAI 兼容接口,这里不限制具体厂商,只要是支持 function calling 的模型都行。
需求先拆成三条核心逻辑:第一,工具层面至少要有搜索和文本整理两个工具,搜索用来拿信息,文本整理用来生成章节初稿;第二,循环必须在信息收集充分后自动切换到写作阶段,不能一直搜下去;第三,必须有轮数上限,防止模型在查资料上无限折腾。
3.2 核心代码实现与逐步拆解
先定义工具层。我给 Agent 准备了两个工具:一个是搜索工具,实际项目里可以对接搜索 API,这里用一个模拟函数代替,重点看循环结构;另一个是 write_section 工具,把文本片段写入报告草稿。
import json from typing import Dict, List, Callable, Any TOOL_REGISTRY: Dict[str, Callable] = {} def register_tool(name: str): def decorator(func: Callable): TOOL_REGISTRY[name] = func return func return decorator @register_tool("web_search") def web_search(query: str) -> Dict[str, Any]: # 真实项目中这里会请求搜索 API,属于 I/O 操作 return {"ok": True, "result": f"关于「{query}」的搜索摘要,包含若干相关链接和要点。"} @register_tool("write_section") def write_section(title: str, content: str) -> Dict[str, Any]: # 真实项目中会写入文件或数据库 return {"ok": True, "result": f"章节「{title}」已写入,共 {len(content)} 字。"}工具返回统一 JSON 结构,是后续所有判断的基础。接下来是核心的循环逻辑:
def build_system_prompt(): prompt = """ 你是一个调研报告助手。请按以下流程完成调研任务: 1. 先使用 web_search 收集资料,每次搜索只查一个关键词。 2. 如果信息不够,继续搜索,但不要重复相同的关键词。 3. 当信息足够时,使用 write_section 写入报告章节,然后输出最终报告。 4. 所有工具调用必须包含合法的 arguments JSON。 如果你的分析已经完成,请直接输出最终报告,并在开头包含「REPORT_END」标记。 """ return prompt.strip() def run_research_agent(task: str, llm, max_steps: int = 20): system_prompt = build_system_prompt() history: List[Dict] = [ {"role": "system", "content": system_prompt}, {"role": "user", "content": task}, ] completed_tool_keys = set() for step in range(max_steps): response = llm(history) # 情况一:模型输出最终报告 content = response.get("content", "") if content: if "REPORT_END" in content: return content, step + 1 history.append({"role": "assistant", "content": content}) # 情况二:模型请求调用工具 tool_calls = response.get("tool_calls", []) if not tool_calls: # 模型既没给内容,也没给工具调用,只能强制终止 return content or "模型未正确终止,强制结束", step + 1 for call in tool_calls: tool_name = call["function"]["name"] raw_args = call["function"]["arguments"] try: args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args except json.JSONDecodeError: history.append({ "role": "tool", "content": json.dumps({"ok": False, "error": f"参数不是合法 JSON,请修复你的 arguments 字符串"}), "tool_call_id": call["id"], }) continue tool_key = f"{tool_name}:{json.dumps(args, sort_keys=True)}" # 重复检测:同一工具同一参数出现两次以上,给模型提示 if tool_key in completed_tool_keys: history.append({ "role": "tool", "content": json.dumps({"ok": False, "error": "你刚刚已经用相同的参数调用过相同工具,请更换策略或停止"}), "tool_call_id": call["id"], }) continue completed_tool_keys.add(tool_key) func = TOOL_REGISTRY.get(tool_name) if func is None: result = {"ok": False, "error": f"未知工具: {tool_name}"} else: try: result = func(**args) except Exception as exc: result = {"ok": False, "error": str(exc)} history.append({ "role": "tool", "content": json.dumps(result, ensure_ascii=False), "tool_call_id": call["id"], }) return "(未在限制轮次内完成)", max_steps这段代码把前面讲的核心设计全部落实了:历史消息持续累积,工具调用结果回填历史,重复调用被检测并反馈给模型,JSON 解析错误也没有直接崩溃而是转给了模型自我修正。它还隐含了终止策略:有 REPORT_END 就给最终结果,没给就强制返回,不让循环无限跑下去。
这里有个细节值得展开说:当模型调用工具后,代码里我并没有立刻结束循环,而是把工具结果追加到历史后,让循环继续执行下一次推理。换句话说,tool reply 不是一棵树的终点,而是下一次决策的起点。很多新手的误区就是工具调用完就不知道如何“把球踢回给模型”,导致整个流程只能走一步。
3.3 可观测性与调试技巧:让每一个决策可回放
手写循环最大的好处是调试透明,前提是你留下了足够的日志。我在这套代码里一向会在几个关键节点打日志:第几轮、模型输出了什么、调用了哪个工具、参数是什么、返回是什么、当前处于哪个阶段。我把日志格式固定下来,方便事后用文本工具分析。
import logging logging.basicConfig( level=logging.INFO, format="%(asctime)s [step %(step)s] %(message)s", )实践中我发现打印每一步的完整对话历史最有价值,而不是只打印最后结果。模型在循环中的每一步判断都受前面所有历史的影响,只看尾部你永远不知道它是被哪条历史带偏的。我常用的调试手法是把 history 完整 dump 成一个 JSON 文件,再在本地开个聊天窗口去“复现”模型的视角。
如果你觉得纯日志不够直观,可以做一个极简的可视化:每轮用“step → 工具名 → 结果说明”的方式打印一行,20 轮以内的 Agent 都能一眼看完全程。这种日志在定位死循环时非常实用——你会立刻看见模型一直在搜同一个关键词,或者一直在写同一节内容。
3.4 成本与性能控制:别让循环烧掉你的钱包
Loop Engineering 一个绕不开的问题:每一次循环都需要一次完整的 LLM 调用,轮数越多,token 消耗越大。一个 15 轮的调研任务,如果每轮都要把几万字的工具结果塞进上下文,单次任务的成本可能高到离谱。
控制成本有几个非常实用的手段,都是我在实际项目里验证过效果的。
第一,工具结果截断。搜索工具返回的长文本,在塞进历史之前先截断到 2000 字以内。真实场景下,搜索摘要里 90% 的内容是废话,保留最前面的关键部分是够用的。我建议在工具层就做截断,不要在循环层做,因为工具最清楚自己返回的信息哪些要紧。
第二,历史摘要化。如果历史超过一定长度,把前面的工具结果用一个“摘要消息”替换掉。写一个 summarize 工具也行,直接调用模型压缩也行。这个操作对长任务特别有效,能大幅延长可执行轮数。
第三,缓存重复调用。如果模型在多个轮次内提出了相同的问题,直接复用第一次的工具结果,不要再真实请求一次。我在上一小节的重复检测里已经覆盖了这个逻辑,它不仅防止死循环,也避免了重复的 API 消耗。
第四,让模型更克制的调用工具。在 system prompt 里明确告诉它“每次搜索必须基于已有信息提出新的增量关键词,不得重复搜索”。好的 prompt 能把轮数压缩掉 30% 到 50%,这在批跑任务时是巨大的节省。
我实际跑过一轮对比:同一个调研任务,不做任何控制时模型烧了约 4 万 token,加了截断和重复检测之后不到 1.6 万 token。差距主要就是循环控制带来的。
4. 真实踩坑记录:循环 Agent 的典型问题与排查思路
4.1 模型陷入“工具调用死循环”
这是新手遇到最多的问题,症状很典型:日志里全是同一对工具来回调用,比如 web_search 和 write_section 反复交替,或者一模一样的关键词被搜了七八次。
原因通常有两个:一是模型确实忘了自己之前搜过什么,这属于上下文注意力不足,只能靠外部记忆来补;二是工具返回的结果没有提供“下一步依据”,模型没有足够信息判断该结束了。
排查的时候,我会先把完整的工具调用序列打出来,看模式:是不是同一个“工具名+参数”组合在重复?如果是,就是重复检测没生效;如果工具参数每次都在变,但整体没有进展,那就是模型在无意义探索,需要给反馈信息加更明确的目标指引。
我的解决方案就是在循环层加重复目标检测,并且把提示写得具体到“你刚才已经用相同参数搜索过,请基于已有结果提出新的搜索关键词,或者直接进入报告写作阶段”。好模型的自我修正能力远超想象,很多时候只是缺一句“有人盯着它”的提醒。
4.2 上下文爆炸:token 被历史消息吃光
循环跑多了,最直接的问题就是历史消息越来越长。模型每轮都要读完全部历史,token 消耗随轮数指数增长,而且一旦超出上下文窗口就会报错。
我踩过一次比较严重的坑:做一个网页抓取 Agent,每轮都把整页 HTML 塞进历史,跑到第五轮时上下文直接爆了,前面的搜索结果全被截断,模型完全失忆。解决办法是在工具层就做清洗和截断,HTML 转纯文本,然后限制最长长度。
上下文爆炸不仅仅是技术问题,也是成本问题。我建议对工具返回设置一个硬长度上限,超过部分直接切掉,同时在 System Prompt 里说明“工具结果可能被截断,需要完整信息时请重新搜索更有针对性的关键词”。这样模型不会因为信息不全而反复抓瞎。
4.3 工具输出格式导致解析失败
模型返回的 tool_calls 不一定永远规范,尤其在一些开源模型上,function arguments 经常不是合法 JSON,或者参数名跟工具定义不完全一致。如果放任不管,循环就会在这里卡死。
处理方式是在循环层做一个容错解析:先尝试标准 json.loads,失败时用一个宽松的解析函数,把 key 和 value 用正则提取出来。如果还是失败,不要直接报错,而是把“参数解析失败”作为工具结果返回给模型,让它自己修复。
更隐蔽的问题在于模型把工具名写错,比如定义的是 web_search,它写成 search_web。发生这种情况,我会在错误反馈里附上可用工具列表,让模型自己读一遍再重新生成。实测下来,大部分模型看到自己手里的“菜单”后会立刻纠正。
4.4 任务没完成就提前收尾
有些模型在信息明显不足的情况下,宁可输出一份泛泛而谈的报告也不继续搜索。这通常是懒惰,不是能力不足。我遇到过一个任务:让 Agent 调研某一垂直领域的市场规模,它搜了一次数据来源就写完了,结果报告里全是定性描述,一个具体数字都没有。
要解决“提前收尾”,我尝试过几种办法。最有效的有两个:一是要求模型在最终报告里包含完整的数据来源,如果没有来源就不允许输出 REPORT_END 标记;二是在循环层增加一个“完成度校验”的子模型,专门评估最终报告的质量,不合格就把评估意见塞回历史让模型补做。
完成度校验增加了额外的 LLM 调用,但值得。尤其在生产场景里,与其让一份烂报告直接交付给用户,不如多花几百 token 把质量兜住。当然,它对 Prompt 的要求也更高,你需要很明确地告诉评估子模型“什么样的报告算合格”,否则它会跟主要模型互相拍马屁,两人都觉得写得不错。
下面是几个高频问题的速查表,我直接把它贴进代码注释里,遇到问题先对照一遍。
| 症状 | 常见原因 | 排查方法 | 处理建议 |
|---|---|---|---|
| 连续调用同一工具 | 缺重复检测 | 打印历史中的工具调用签名 | 添加重复签名检测并反馈给模型 |
| 上下文越来越长 | 无窗口控制 | 监控每轮 token 消耗 | 工具结果截断 + 历史摘要化 |
| 参数 JSON 解析报错 | 模型输出格式不稳定 | 打印原始 arguments 字符串 | 容错解析 + 向模型回传错误信息 |
| 未完成就输出报告 | 缺少完成度标准 | 人工抽查最终报告质量 | 加入完成度校验子模型 |
| 轮数超限仍不停止 | 安全阀缺失 | 检查循环代码的 break 条件 | 强制 max_steps 硬上限 |
| 工具超时导致卡死 | 外部 API 不稳定 | 检查耗时日志 | 给工具调用加超时和异常捕获 |
4.5 思考式排查法:当模型不按规则出牌怎么办
最后分享一个偏“玄学”但很实用的排查思路:当你无法从代码层面判断模型行为是否合理时,把历史导出来自己扮演一次模型——也就是所谓的“角色扮演回放”。
具体做法是这样的:把某一轮的完整历史里,模型消息和工具返回打印出来,人工去看模型在最后一步的选择,假设你是这个模型,你会如何决策?如果连你自己都觉得信息足够、应该进入下一阶段,那说明模型的判断是合理的,问题可能出在终止条件上;如果连你自己都觉得信息不足,那问题出在工具的数据质量上,模型确实没法从垃圾数据里推理出有价值的东西。
这个方法帮我定位过很多诡异的问题,比如模型在一轮里突然调用了一个跟任务完全无关的工具,回看一下才发现是前面某条工具结果里的字符误导了它。纸面上的日志永远比脑子里模糊的印象可靠。
5. 继续加码:从单循环到多循环的扩展方向
前面这套调研报告 Agent 用的还是单循环结构:一个主模型驱动所有决策。但真实世界里的复杂任务,往往需要多个循环配合,这也是 Loop Engineering 的核心进阶方向。
第一个常见的扩展是“规划器—执行器”双层循环。规划器模型负责拆解任务、生成执行计划,执行器模型负责跑具体的工具,每完成一个子步骤,规划器收到结果后重新评估剩余规划,直到全部结束。这个结构能显著提升复杂任务的稳定性,因为规划器有全局视角,执行器可以专心做局部操作。
第二个扩展是“子 Agent 循环”。当工具调用需要上下文隔离时,我会为每个子任务单独开一个循环,让它们各自维护自己的历史,最后汇总到主循环。比如调研任务里,每个细分领域开一个子 Agent 循环分别去收集资料,主循环负责合并结果和写报告。隔离的上下文不仅降低了彼此的干扰,也大幅减少了主循环的 token 消耗。
第三个扩展是“记忆循环”。如果让 Agent 在多轮任务中记住用户偏好或历史决策,就需要一个专门的管理循环来维护长期记忆。每轮决策结束后,主循环把关键信息抽取出来写入记忆库,下一次循环开始时把记忆加载进来。这个本质上是在循环外面套了一层“记忆维护循环”,两者同步运转。
如果你已经有了一定的基础,我特别建议自己先手写两三个不同场景的单循环 Agent,把状态维护和终止条件玩明白,然后再去用 LangGraph 这样的框架。框架帮你省掉了大量样板代码,但它也隐藏了循环内部的很多细节。做过一遍手写循环之后,你看到 LangGraph 的 StateGraph 和 conditional edges,就会有一种“这不就是我把 if-else 整理成配置”的感觉,理解成本瞬间归零。
说到最后,聊聊我做 Loop Engineering 的个人体会。它不是一个你调一晚上就能学会的 API,而是一种工程思维的转变:别再想着靠某一次完美的 Prompt 让模型一步到位,而是接受“模型会犯错、会绕路、会自我怀疑”这个事实,然后用循环把这些错误变成可修正的中间状态。每给循环加一道“护栏”,你其实就在慢慢把一个演示用的 Agent 变成能稳定跑业务的系统。这个过程中最值得投入的地方,不是复杂的状态机理论,而是对真实任务的拆解能力和对日志的敏感度。数据永远能告诉你答案,前提是你让它在循环里流动起来。