一、Loop工程
一、Loop工程
前言
博主在Agent发展历程中使用过各种智能体,随着LLM的发展曾认为最终只需要一个通用智能体即可解决所有问题,但很快我又发现在各类垂直领域依然需要自己的智能体。
博主从24年开始便尝试进行垂直领域智能体开发,从最开始的基于循环事件hook的chat bot,再到LangChain、LangGraph等一类workflow编排类框架,再到如今的完整Harness工程。最初以为只需要选最好的模型、融最多的知识、给最多的工具即可,但实际开发下来效果却不尽人意,同时对于细节的把控不到位也会让后期的迭代无从下手。这期间愈发体会到要把LLM按照设计好的路径行进并不是一件容易的事情。
本系列博文记录关于Harness开发的一些笔记与心得。我还是希望无论是谁都能从中获得一些帮助,所以我希望本系列尽量囊括Harness全线流程,作为该系列的第一篇博文,我决定从Loop工程开始介绍,
Loop工程
最小Loop实践
让我们从一个最小的循环设计开始,考虑一个没有思维模型的LLM,自然我们可以想到多轮对话的方式来实现一个Loop
多轮对话中的上下文累积 即将历史对话作为背景上下文,和新的问题一起输入给LLM,得到回答后更新上下文的调度循环。
然而,LLM本质上并不会多轮对话这件事,如果你有过本地部署LLM的经验便会知道这个
msgs数组并不是直接输入给LLM的,它将由聊天模板进行装置处理成标记本文,例如<|im_start|>system 用中文回答。<|im_end|> <|im_start|>user 什么是 Loop?<|im_end|> <|im_start|>assistant也就是所谓的对话历史其实只是一段文本,LLM在训练中学会识别这类格式,然后将预测续写这个段落,最终再切分渲染为模型的回复。
对于思维模型以及有工具调用能力的模型,要制作一个Loop的过程也是类似。我们会在上下文中除了原有的msgs外加入tool schema,里面包含各个工具的函数原型。
模型输出时同样会用特殊的标签标记思维链部分以及选择调用的工具。
而我们的Harness则需要手动处理这类情况,首先应该处理模型所选择调用的工具并运行,得到结果后我们可以将思维链、工具调用、调用结果一起拼接到上下文中再次请求模型继续处理,直至得到完整回复。

DeepSeek工具调用例图 实际上这也是主流的 ReAct 式循环的基本骨架,让模型以Thought、Action、Observation 形式组织执行轨迹,Agent通过与环境的多次交互逐步推进,模型不必在最开始就确定全部步骤。
用户目标 → 分析当前信息 → 选择行动 → 执行并获得观察 ↑ ↓ └──── 根据反馈调整 ─┘到此时,我们算是完成了一个最小化的Loop工程实现,这个Agent将能够记住我们的历史消息,能够调用工具影响环境。后续关于Harness的所有内容可以说都是由此而展开的。
Execution boundary
对于一个轮次(通常称为Turn),可能会包含多次模型调用,常规的思维链和工具调用只需要Harness按照上一节的内容处理更新上下文再请求即可。
最小实现里,Loop 往往只有“继续”和“退出”两个分支,但随着交互场景增加,仅靠一个布尔值很难描述当前发生了什么。有些时候模型可能会认为缺少信息要求用户补充信息,或是因为网络失败、进程丢失等影响中断Turn,此时,Harness还需要处理等待与恢复。
状态 含义 运行中 正在请求模型、执行工具或处理结果 等待中 正在等待用户输入、确认或后台任务结果 已完成 本次运行达到了约定的正常结束条件 已失败 遇到了当前策略无法继续恢复的错误 已取消 用户要求终止,相关停止处理已经完成 等待时需要保留尚未完成的事项,并明确由哪个事件唤醒执行。例如,等待用户确认和等待测试进程结束,虽然都暂时无法继续,但恢复所需的事件不同。记录状态时,除了状态本身,还可以保存退出原因、待处理动作以及相关工具任务的标识。
同时由于Agent对环境的影响并不是无副作用的,在工具调用时的中断Harness需要做更多的步骤处理这些内容,例如中断发送在模型请求修改文件,此时可能有几种状态:
- 请求发出,Harness收到,实际工具调用还未执行时中断;
- Harness收到请求并且开始执行中发生中断;
- 请求执行完毕,还未将请求结果发送给LLM时发送中断;
- 异步任务,中断时后台任务仍在运行。
显然对于第一类情况常规的处理方式可以是保存待调用请求重新运行时恢复工具调用,也可以时直接丢弃这次调用,恢复时从上一次模型调用的上下文开始重新请求。
对于第二/三类情况则稍显复杂,最简单的做法时完全不关心这些副作用,下次恢复时候直接重试或者丢弃该请求。要对这类被打断的调用处理好收尾工作,更好的做法是等待第二类情况执行完毕转换为第三类情况同时记录中断状态,下一次将请求结果和中断状态发送给LLM等待LLM判断下一步操作。
当然,有些时候的中断可能是进程崩溃的硬中断,此时Harness在恢复时应该根据当前状态传递不同的状态给LLM,例如有调用开始记录但是没有完成结果,可以单独补充一个异常状态并提示LLM此时应该先核查环境情况再继续进行下一步。
对于第四类异步任务的情况,我将在下一节介绍关于异步工具调用的设计与实现。
总的来说,Harness在实现Loop中的工具调用时候,不仅仅只是作为一个路由将LLM的工具请求转发到具体工具并回送结果,还需要保存工具的执行记录(如标识、名称、参数、状态、结果等),还需要处理工具调用中可能的异常情况。 同时在不同场景下的处理不能全部一概而论,例如部分支持幂等的服务可以减少处理流程加快效率,或者是利用hash在写操作前检查一致性等方式减少冲突。
到这里可以看出,Harness工程在设计时可以有很多天马行空的创意,但到实际落地时其实有非常多的脏累细节需要根据应用场景单独设计与优化。
Tool calls
工具调用并不总是顺利的,当调用某个工具出现错误时,Harness要做的不仅是把错误结果返回给LLM,在垂直场景中,Harness往往需要对错误进行进一步的细化。
例如判断错误是可以接受的还是不能接受的,如果只是参数错误、环境变更等原因造成的可以直接交给LLM再次运行,而如果是依赖缺失、外部资源受限等原因造成Harness可以考虑直接进行中断并提示用户需要进一步操作。
无论如何,当遇到调用错误时,Harness的返回给LLM的内容应当包含错误类型、必要原因和相关结果,而不是直接错误输出,让模型能够据此选择下一步。
在Agent执行任务时,主要的时延由以下几部分构成
总时间 = LLM请求时延 + LLM推理时间 + Harness处理时延 + 工具执行时间对于同一个LLM,要减少Agent运行同一个任务的时间,Harness要做的便是在上述四个方面进行优化。显然Harness需要减少请求的参数,同时尽可能提高工具执行的效率。
我们可以很容易从网络协议的设计中找到灵感,即批处理机制,例如TCP中的滑动窗口和累计确认,都是为了减少网络通信次数。现代LLM在训练时其实就已经在教模型应尽量执行多个工具调用,或是在一个工具调用中尽量执行多个操作。在Harness中也可以在提示词中引导模型批量提出工具调用。
然而,并不是所有的工具调用都能直接批量并行,工具调用之间本身也可能存在依赖关系,对同一资源的并发操作也可能存在额外约束。Harness需要根据工具之间的依赖关系和资源限制编排工具执行顺序,一般来说,我们可以按照
读-写-执行的顺序进行编排。有些工具几秒钟就能完成,有些则需要运行几分钟或者更久。此时若始终等待它们结束,整个Loop调度将被这些最慢的命令所拖累导致效率低下。

异步ReAct,图片来源于DeepSeek的Super-agent 此时我们可以引入异步ReAct的概念,和异步任务一样,我们可以单独维护一个事件循环,让工具调用时直接返回异步事件而不等待,当异步事件执行完毕再向会话进程发送消息将运行结果返回给LLM,同时也可以额外给LLM提供await / read / stop 等工具以便LLM能够主动控制这些异步任务。
很显然我们并不能将所有的调用都设置为异步,主要有两个原因:
- 大部分调度可能是强依赖,如果没有得到结果LLM无法进行更进一步的判断,此时在任务结束之前LLM可能只会返回垃圾信息空耗上下文。
- 一些工具本身执行很快,而进行异步处理同样是需要额外代价的。
所以,在实际的设计实现中,需要根据具体场景来设计哪些工具需要进行异步处理,在通用场景下,以DSH为例,当一个普通调用5s还未结束时则转为异步任务处理。
Resource limit
我们当然希望每一次Turn都能完成用户所输入的问题,所以我们希望Agent能穷尽各类可能进行实现;我们又希望Agent能够在知道任务可能不可行或是有其他方向时主动停止,减少资源浪费提高效率。
显然这是两件矛盾的事,在Harness设计中我们需要在二者之间找到一个平衡,除了依赖模型主动结束,Harness 还需要设置自己的执行预算。初步可以从几个维度进行约束:
- 模型调用轮数和工具调用次数,限制任务能够尝试多少次。
- 单次调用超时和整个任务的截止时间,限制等待与执行时长。
- 输入长度、单次输出和累计 Token 用量,分别管理上下文容量与生成消耗。
这些限制需要作用到整个运行过程。在接近软上限时,可以添加额外的提示词模型尽快收尾;达到硬上限后,则由程序停止继续调度并收尾。
此外,或许我们想要的是Agent重复调用或者无进展循环时候能够自动停止,那么此时只设置一个总的限制是做不到了,此时可以考虑依赖外部额外的观察者实现,由于不是本篇的主题不在此展开。
Interaction
用户不一定会等 Agent 完成任务后再发消息。执行过程Agent可能会调用ask_question类工具要求用户输入,用户也可能主动出现补充要求、纠正信息等。Harness工程还需要解决这些输入如何进入正在运行的 Loop。
一种直接的处理方式是先将新消息放入队列,在下一次模型调用前统一纳入上下文。这样的好处是即时且整个消息仍然在一次Turn中。
如果要求更高的即时性,则需要记录当前位置的上下文版本和进行中的事件,然后直接将插入消息发送给LLM,此时LLM可以根据新消息选择还在运行中的事件是否可以继续还是直接取消掉,Harness则根据LLM返回对这些事件做进一步处理。
当用户取消一个任务,涉及的工作可能比退出主循环更多。Harness除了需要停止新的模型请求和工具调度外,还需要向在途调用发送取消信号,处理本次运行拥有的后台任务,并释放不再需要的资源。(注:这也是很多Agent内存泄漏的原因,例如Codex后台git进程完成后没有清理)
在具体实现中,这件事其实并不是那么容易,例如在Python中取消异步任务的等待协程,外部进程可能依然还在运行,Harness在实现中应该要跟踪操作是否真正完成并加入兜底操作。
