首页 / 文章

Agent Harness、Loop与Graph:可靠AI智能体背后的三层架构

为何不可靠的代理通常会缺失一层结构:模型周围的约束框架、基于证据的循环机制以及明确的图结构,同时还包括代码模板和调试检查清单。

发布者:REACTAPP.TOP阅读约 15 分钟3454 词

来源: Harness, Loop, and Graph: What I Learned Building the Same AI Agent Three Different Ways

听起来熟悉却其实不甚了解的名称

“智能体驾驭工程”和“循环工程”这类表述在社交媒体帖子及博客文章中出现得如此频繁,以至于人们开始以为自己已理解它们。然而熟悉感具有欺骗性,小小的故障往往就能暴露这一点。

想象一个负责获取arXiv论文、对其进行提炼并编制每周摘要的研究智能体。前三次测试都很顺利。随后,一次受到速率限制的API调用在毫无征兆的情况下失败了,代码中也没有任何异常提示,于是该智能体开始编造那些它从未阅读过的论文的摘要。问题并不出在模型上,提示语也没有问题。真正缺失的是让相关代码能够区分真正读过的论文与看似合理的虚构内容的机制。这一缺陷确实存在,它也有专门的名称,而仅仅调整提示语是无法解决它的。

将同一个智能体分别构建三次,就能清晰地体现出其中的差异:一次优化是针对环境,另一次针对反馈循环,第三次则用于对工作流程进行显式控制。以目前的形式来看,这一领域仅发展了大约十八个月,因此其名称仍存在争议。不过,这些本质上的差异确实存在,并且对应着实际的工程决策。这样一来,当智能体出现故障时,就能明确应该先修复三个层次中的哪一个。

单独来看,语言模型只是实现文本到文本的转换。只有在用户提供的前提下,它才能在多次调用之间保持记忆。文件访问、shell命令以及API调用都需要有人去构建相应的基础设施,而且没有外部验证,模型也无法确认自己的答案是否正确。2024年那些正在开发智能体的人,在首次遇到意外用户操作时,就发现了这个问题——原本流畅的演示立刻就出故障了。

该行业对此的应对措施是不再将“代理”视为一个整体,而是将其拆分为模型之上三个独立的工程问题。

LAYER            QUESTION IT ANSWERS                         FAILS AS
----------------------------------------------------------------------
Harness           What can the agent see and touch?          Missing tools,
                   (environment, tools, memory,               lost state, blown
                   permissions, execution limits)             budgets, leaks
----------------------------------------------------------------------
Loop               How does one attempt turn into a           Confident wrong
                   correct one? (retry, verify, stop)          answers, infinite
                                                                spinning, silent
                                                                failure
----------------------------------------------------------------------
Graph              What is the actual shape of the work?       Wrong step order,
                   (sequence, branches, parallel paths,         no recovery path,
                   human checkpoints)                           no visibility
----------------------------------------------------------------------

理解每一层最简单的方法是先构建最简版本,使其出错,然后再阅读相关研究文档和框架文档,了解其出错的原因。

工具集:智能体能够看到和触碰的内容

最清晰的定义出现在 LangChain 的一份工程笔记中,该笔记将这一概念简化为一个公式:智能体等于模型加上工具集。模型负责提供推理能力,而工具集则提供推理能力作用于现实世界所需的一切,包括工具、内存、上下文、权限,以及防止其做出代价高昂或危险行为的约束机制。

一种常见的早期“为智能体提供工具”的思维模式是编写几个Python函数并在系统提示中对其进行描述。这种做法适用于演示,但无法应对超过一个上下文窗口长度的任务,也无法处理在凌晨2点无人监控时失败的工具调用。实际上,这种框架需要承担六项职责。

  • 上下文注入决定了模型在每个推理步骤之前所看到的内容:用户的消息,以及检索到的文档、之前的对话、项目状态和组织政策。过度修正很容易导致问题。将整个文件树和git日志都放入每次调用中会降低准确性,因为模型会被大量无关信息淹没,而经过筛选的较小上下文通常比庞大的原始上下文更有效。
  • 动作接口将决策转化为现实世界中的具体操作,无论是API请求、Shell命令、数据库写入还是浏览器点击。模型上下文协议正是为此而存在,且今年有了诸多变化。它始于2024年末,作为智能体查找并调用工具的标准方式,让团队无需为每个API单独进行集成开发。到2026年7月的规范版本时,它已拥有可在普通负载均衡器后运行的无状态核心,无需粘性会话;同时还有来自AWS的任务扩展功能,用于处理超出单次请求范围的作业;此外还基于OAuth和OpenID Connect建立了更严格的授权机制。相关数据表明,目前已有超过一万台公共生产服务器在使用该协议,其SDK的月下载量更是超过9700万次。若要在非MCP或类似协议的工具层之上构建集成框架,就必须有充分的理由。
  • 持久化指的是那些能够在重启、崩溃或整个上下文窗口关闭后依然存在的元素,例如检查点、会话状态、向量内存以及 Git 历史记录。最初的智能体并不保存这些数据,因此一旦连接中断,40 分钟的进展就会丢失。在每个子任务完成后写入小型 JSON 检查点,其对可靠性的提升效果远超过任何提示词修改。
  • 执行控制模块包含了重试规则、超时设置、令牌与消耗预算、模型路由机制、对生成的子智能体的限制,以及需要人工确认的步骤。如果没有这一模块,智能体若陷入反复调用却失败的循环,整个任务的处理时间可能会被无限拉长。
  • 安全与治理方面包括对工具的最小权限访问控制、机密信息处理、沙箱环境使用、审计日志以及允许列表管理。模型生成的代码始终应在沙箱环境中运行。曾有编程智能体误将某个文件夹当作构建产物,试图对其执行rm -rf命令,最终被及时制止。该行为并无恶意,只是因错误加上相应的破坏权限所导致。
  • 可观测性涉及追踪信息、工具调用日志、令牌使用情况、延迟数据以及评估结果。在智能体在生产环境中开始出现异常行为,且实际操作记录与预期不符时,这些数据才会显得至关重要。
  • 下面是一个功能完备但结构简单的Python示例程序。它由本地的Ollama模型驱动,因此无需API密钥,而要切换到托管模型只需一行代码即可。

    import json
    import subprocess
    import requests
    from datetime import datetime
    OLLAMA_URL = "http://localhost:11434/api/chat"
    MODEL = "llama3.1"
    # --- Action surface: the only two things this agent is allowed to do ---
    def read_file(path: str) -> str:
        with open(path, "r") as f:
            return f.read()[:4000]  # cap it, don't blow the context window
    def run_tests() -> str:
        result = subprocess.run(
            ["pytest", "-q"], capture_output=True, text=True, timeout=60
        )
        return result.stdout[-2000:] + result.stderr[-2000:]
    TOOLS = {"read_file": read_file, "run_tests": run_tests}
    TOOL_SCHEMA = [
        {
            "type": "function",
            "function": {
                "name": "read_file",
                "description": "Read a file from the project",
                "parameters": {
                    "type": "object",
                    "properties": {"path": {"type": "string"}},
                    "required": ["path"],
                },
            },
        },
        {
            "type": "function",
            "function": {
                "name": "run_tests",
                "description": "Run the project's pytest suite",
                "parameters": {"type": "object", "properties": {}},
            },
        },
    ]
    # --- Persistence: cheap but real ---
    def checkpoint(state: dict, path: str = "checkpoint.json"):
        with open(path, "w") as f:
            json.dump({**state, "saved_at": datetime.utcnow().isoformat()}, f)
    # --- Execution control: hard ceilings, not suggestions ---
    MAX_TOOL_CALLS = 8
    def harness_run(user_task: str):
        messages = [{"role": "user", "content": user_task}]
        calls_made = 0
        while calls_made < MAX_TOOL_CALLS:
            response = requests.post(
                OLLAMA_URL,
                json={"model": MODEL, "messages": messages, "tools": TOOL_SCHEMA, "stream": False},
            ).json()
            msg = response["message"]
            messages.append(msg)
            if not msg.get("tool_calls"):
                return msg["content"]  # model is done, no more tools needed
            for call in msg["tool_calls"]:
                name = call["function"]["name"]
                args = call["function"].get("arguments", {})
                if name not in TOOLS:
                    result = f"error: tool {name} is not permitted"
                else:
                    result = TOOLS[name](**args)
                messages.append({"role": "tool", "content": str(result)})
                calls_made += 1
                checkpoint({"messages": messages, "calls_made": calls_made})
        return "stopped: hit the tool call ceiling before finishing"
    

    这里没有任何花哨的设计,这正是其中的教训:对于一个不负责记录数据的模型,框架需要通过精细的机制来管理相关操作。对于托管模型,应用 OpenAI 或 Anthropic 的 SDK 调用替换 requests.post 代码块。由于这些机制并不依赖由谁来响应请求,因此工具的架构和检查点机制无需改动。

    关于模型变笨的抱怨几乎总与框架有关。可能是工具返回了过时的数据,上下文被无关的历史信息充斥,或是权限设置得过宽或过窄,导致智能体不断尝试它本就不被允许的操作。在编辑提示词或更换模型之前,先检查框架,因为问题通常出在那里。

    循环机制:如何让一次尝试变得有效

    如果约束机制决定了智能体可以触碰什么,那么循环机制则决定了它如何从第一次尝试逐步得到良好结果。这一层往往被低估,因为“让它重试即可”听起来很简单,直到智能体真正开始重试之后才会发现问题。

    典型的失败案例是那种自我纠正的循环:在模型给出答案后,它会询问自己是否确信该答案是正确的。而模型的回答永远是“是”。在没有外部反馈的情况下,模型对自己生成的答案进行评分时几乎总能给出正确评价,因为它没有任何可以用来判断错误的参照标准。这是一个有大量文献记载的问题,而非偶发的错误——没有错误参照标准的循环总会认为自己已经成功了。

    解决之道是不再让模型自行评分,而是为该循环引入外部评判标准:包括能判定通过或失败的测试套件、模式验证工具、编译器、代码检查规则,或是能够对初始答案进行补充或反驳的第二个独立检索系统。基于证据的循环会关注现实世界的状态,而基于置信度的循环则关注模型的自我感受。

    Ralph Wiggum 技术及其为何有效

    2025年,工程师杰弗里·亨特利提出了名为“Ralph Wiggum”的模式后,它便广为流传。简而言之,这种方法的原理极为基础:通过bash循环不断将相同的提示发送给编程智能体,直至任务完成,进度则通过文件和git提交来记录,而非依赖模型对对话内容的记忆。

    #!/bin/bash
    # ralph.sh - crude but surprisingly effective
    while true; do
      claude -p "$(cat PROMPT.md)" --dangerously-skip-permissions
      if git diff --quiet HEAD~1 HEAD -- STATUS.md && grep -q "DONE" STATUS.md; then
        echo "Task complete."
        break
      fi
      sleep 5
    done
    

    看起来这方法根本行不通,但在经过简单重构后,它的粗糙之处反而成了优势。每次处理时都丢弃对话上下文,强制将进展记录到 Git 历史和状态文件中,这样可以避免上下文失效的问题,也能防止代理误以为自己已经完成了实际上并未完成的任务。虽然这种方法并不优雅,但由于 Git 和文件系统不会为了讨好模型而说谎,因此它意外地具备了以证据为依据的特性。

    下面是一个类似的小型以证据为依据的循环示例,同样可以在本地针对 Ollama 运行,它将测试失败视为“尚未完成”这一状态的唯一合理解释。

    def evidence_loop(task_description: str, max_attempts: int = 5):
        attempt = 0
        history = []
        while attempt < max_attempts:
            attempt += 1
            code = generate_patch(task_description, history)   # calls the model
            apply_patch(code)
            test_output = run_tests()                          # external evidence
            passed = "failed" not in test_output.lower()
            history.append({
                "attempt": attempt,
                "patch": code,
                "test_output": test_output,
                "passed": passed,
            })
            if passed:
                return {"status": "success", "attempts": attempt}
            # feed the *actual failure*, not a vague "try again"
            task_description = (
                f"{task_description}\n\nPrevious attempt failed with:\n{test_output}\n"
                "Fix the specific failure above."
            )
        return {"status": "gave_up", "attempts": attempt, "history": history}
    

    请看返回之前的最后一行,因为它蕴含了核心思想。循环并没有要求“更加努力”,而是将真实的测试失败案例输入模型,因为正是这些具体的外部证据使得第二次尝试优于第一次,而非单纯的随机重试。在大多数智能体中,最大的可靠性提升来自于用适合该任务的真正外部验证方式,取代“这看起来对吗?”这样的判断。

    同时,知道何时停止也至关重要。一个没有尝试次数、成本或时间上限的循环并不能构成一个稳健的智能体,它只不过是个即将引发问题的漏洞罢了。

    图表:当任务存在真实分支时

    约束机制负责管理智能体的行为,而循环结构则使其能够通过一次次尝试不断改进,但二者都无法描述那种类似树形结构的任务:先执行某项操作,再根据结果从三种动作中选择一种,等待人类确认,随后并行执行两项检查并将结果合并。

    这就是图引擎技术。人们往往最抗拒这一层,因为循环在很长一段时间内看起来就足够用了,但当工作并非朝着单一通过标准直线推进时,循环就不再适用了。循环只擅长一件事:不断尝试、验证并修改,直到满足某个条件。而当下一步需要根据多个结果中的哪一个来决定、当两个子任务可以同时进行、或者需要有人批准才能继续时,循环就显得力不从心了。有些团队试图把所有这些内容都塞进一个提示词中,指望模型能自行处理,虽然这种方法曾奏效,但极其脆弱。一旦出现分支结构,控制流就应该放在明确的结构中,而不是由模型每次随机应变地决定。

    图引擎使工作流程变得清晰可见。节点代表各个步骤,如代理调用、工具调用、验证器操作或人工检查点,而边则代表将执行流程从一个节点推进到下一个节点的条件。通过查看该图,可以准确了解任务所走的路径,这在半夜发生生产故障时尤为重要,因为此时你需要知道实际发生了什么,而非凭猜测。

    LangGraph是此场景下的常用选择。它以有向图的形式表示工作流程,能在节点间传递带类型信息的状态,利用条件边实现分支,同时还内置了检查点功能,使得任务可以暂停、重启或恢复到之前的状态。

    from langgraph.graph import StateGraph, END
    from typing import TypedDict
    
    class AgentState(TypedDict):
        task: str
        draft: str
        review_passed: bool
        attempts: int
    def draft_node(state: AgentState) -> AgentState:
        state["draft"] = generate_draft(state["task"])
        return state
    def review_node(state: AgentState) -> AgentState:
        state["review_passed"] = run_validator(state["draft"])
        state["attempts"] += 1
        return state
    def needs_human(state: AgentState) -> str:
        if state["review_passed"]:
            return "approved"
        if state["attempts"] >= 3:
            return "escalate"   # branch: give up automating, ask a human
        return "retry"
    graph = StateGraph(AgentState)
    graph.add_node("draft", draft_node)
    graph.add_node("review", review_node)
    graph.set_entry_point("draft")
    graph.add_edge("draft", "review")
    graph.add_conditional_edges(
        "review",
        needs_human,
        {"approved": END, "retry": "draft", "escalate": "human_approval"},
    )
    

    普通的循环结构很难清晰地描述其功能:一次检查会产生三种结果(完成、重试、停止并请求人工干预),每种结果都会导向不同的处理路径。这种升级路径非常重要——它决定了智能体是在第三次失败后默默放弃,还是将所有尝试过的历史记录完整呈现给人工。

    出于同样的原因,微软的AutoGen今年也加入了类似功能,名为GraphFlow。多个智能体或分支式工作流会让隐式的循环结构难以理解,而图结构则可以直接被查看。

    为了不依赖营销页面,而是真正判断各个框架的优势,我们在一个小型项目中测试了四种框架:该项目包含一个带有验证分支和人工审批环节的文档审核流程。

    FRAMEWORK          ORCHESTRATION MODEL         STRONGEST AT               STATE / PERSISTENCE
    --------------------------------------------------------------------------------------------
    LangGraph           Directed graph, typed        Complex branching,        Built-in checkpointing,
                         state, conditional edges     long-running workflows,   time-travel debugging
                                                       human-in-the-loop
    --------------------------------------------------------------------------------------------
    CrewAI               Role-based "crew" of         Fast prototyping,         Sequential task output
                          agents, sequential or        multi-agent brainstorm   passing, lighter state
                          hierarchical process         style collaboration      model
    --------------------------------------------------------------------------------------------
    OpenAI Agents SDK     Explicit agent handoffs      Minimal-friction start   Ephemeral context
                                                        inside the OpenAI        variables by default
                                                        ecosystem
    --------------------------------------------------------------------------------------------
    AutoGen / GraphFlow   Multi-agent conversation     Agent-to-agent           Conversation history
                           plus optional graph layer   negotiation, graph-      plus graph state
                           for explicit control        based orchestration
    --------------------------------------------------------------------------------------------
    

    其实用价值在于:如果工作流程能够以步骤的扁平列表形式展示在一页上,就无需使用图框架,通过精心设计的循环结构即可完成任务,且维护成本更低。一旦工作流程出现分支,图框架的优势就会立刻显现,主要体现在节省调试时间上,而非运行时的性能提升。

    三层结构的层次关系

    这些并非相互竞争的方案,而是垂直堆叠的关系。以下是在同时使用这三层技术后的结构示意图。

    +--------------------------------------------------------------+
    |  GRAPH   - decides which node runs next, where branches      |
    |            are, where a human has to approve                 |
    +--------------------------------------------------------------+
    |  LOOP    - inside a given node, drives attempt -> verify ->  |
    |            revise until the evidence says stop               |
    +--------------------------------------------------------------+
    |  HARNESS - underneath both, provides the tools, memory,      |
    |            permissions, and limits that make any of it       |
    |            possible to execute safely                        |
    +--------------------------------------------------------------+
    |  MODEL   - the reasoning engine none of the above exists     |
    |            without                                           |
    +--------------------------------------------------------------+
    

    在 LangGraph 工作流中,某个草稿节点可以尝试多次基于证据的循环处理,之后才将控制权交还给整个图结构。这个内部循环之所以能够运行,是因为其底层的框架允许模型执行测试、读取文件并调取之前的尝试结果。一旦移除任意一层,其余部分就会停止工作,因此要让一个整体式的“智能体”更难被操控,其效果终究会达到瓶颈。

    采用三层结构思考能显著加快调试速度。在修改提示词之前,应按顺序检查以下各项。

    SYMPTOM                                    LIKELY LAYER TO CHECK FIRST
    ----------------------------------------------------------------------
    Agent doesn't know something it should      Harness -> context injection
    Agent "can't" do something it should be     Harness -> action surface /
      able to do                                 permissions
    Agent forgets earlier progress after a      Harness -> persistence
      restart or long run
    Costs or run time spiral unexpectedly       Harness -> execution control
    Agent confidently reports success on        Loop -> swap confidence checks
      work that's actually wrong                 for external evidence
    Agent retries the same mistake              Loop -> is it seeing the actual
      repeatedly without improving                failure, or just "try again"?
    Agent takes the wrong path when a task      Graph -> is the branching logic
      has more than one possible outcome          explicit, or implicit in a
                                                   single prompt?
    No human ever gets a chance to catch a      Graph -> is there an approval
      bad decision before it ships                node, or is it all automatic?
    

    如果有这样的表格,就不用花一整个周末去把问题归咎于提示词,而实际上问题出在缺失的检查点上。

    未解决的问题

    仍有部分问题尚未解决,现在就断言得比现有证据所允许的更肯定是不正确的。

    首先,相关术语仍在变化。今年之前,“Harness工程”还几乎不存在于大规模应用中,再过一年它可能会被纳入更广泛的“智能体工程”概念之下,而“harness”、“loop”和“graph”则会成为子类别,而非人们专门研究的领域。

    其次,即使Ralph Wiggum方法能够成功,其方式也略显不妥。以简单的循环方式向模型提供单一提示,并仅依赖git历史记录作为其记忆机制,这就好比在缺乏真正持久性的框架上临时修补。它适用于规模较小、范围有限的代码重构,但对于涉及复杂分支或实际后果的场景,还不可完全依赖。

    第三,图框架可能仍在不断演变。LangGraph的检查点功能非常出色,但有些团队仅仅因为潮流才选用整个图框架来处理从不分支的三步流程。若是直线型的工作流,自然可以保持这种状态。

    在所有这些情况中,有一个观点始终成立:不可靠的智能体几乎从来无法通过更优的提示来解决问题。问题几乎总出在三个层次中的某一个,而上述问题正是用来判断具体出在哪个层次的。

    如果今天就开始构建研究代理,那就意味着首先要打造相应的支撑框架,并将其视为重要的承重基础设施,而非事后才考虑的要素。从第一天起就应该采用基于证据的循环机制,而非依赖模型的自我评估,因为仅这一改变就能解决比其他任何方法都更多的问题。至于图结构框架,则要等到能够用纸把工作流程画出来并清晰显示分支点之后再考虑。

    该模型在过去三年里一直备受关注,但实际应用中的可靠性取决于围绕它构建的工程体系。这项工作远没有人们所说的“模型不够智能”那么光鲜,却也更容易处理。实际上大多数时候它都是够智能的,只是尚未获得展现其能力所需的环境、反馈或指导框架而已。

    相关阅读

  • 配置驱动型智能体:构建一个运行时,快速实现功能定制 —— 企业为何应构建一个基础智能体,并通过外部配置(如目录、技能注册表、治理机制、测试工具、LangGraph以及故障模式)来实现功能多样化。
  • 利用、循环与图结构:编码智能体系统的三层嵌套架构 —— 故障现象可帮助确定需要修复的层级。阐述如何实现利用、循环与图结构的嵌套,多智能体在何处发挥作用,对抗性检查的局限性,以及相应的检查清单。
  • 实用指南:MCP 2026:协议的成长历程——以及生产环境中的 AI 是怎样的 —— 为采用该模式的团队提供的实用指南详细操作指南:包括合同、检查项以及可直接插入的代码片段。