Harness Engineering 笔记(二):Agent 运行时核心机制——循环、路由与上下文

上一篇介绍了 Harness Engineering 的官方定义(Context Engineering + Architectural Constraints + Entropy Management 三大支柱),并说明本系列聚焦运行时实现层。这一篇进入 Agent 内部,拆解它的核心运行机制。在 Harness Engineering 的语境下,本篇内容主要对应 Context Engineering (上下文管理)和 Workflow Control(工作流控制)。

Agent 三大运行时机制

Agent 的运行时可以拆解为三个核心机制,它们对应了 Agent 在每一步需要回答的三个问题:

  • 循环控制(Agent Loop) :调用完大模型还要不要继续?→ Workflow Control

  • 输出路由( Output Routing) :LLM 这次输出后应该要做什么?调用工具?还是回答用户?→ Workflow Control

  • 上下文管理(Context Management) :要给模型喂什么信息,该给模型看到什么?→ Context Engineering

总的来说这三者相互协作共同搭建起来一个 Agent 的框架: 上下文管理准备弹药 → LLM 推理 → 输出路由翻译 LLM 的输出结果 → 决策 → 循环控制驱动执行 → 结果回到上下文管理 → 循环继续。

机制一:循环控制(Agent Loop)

Agent Loop 是 Harness 的心跳 ------ 它是由一个 while 循环组成,里面藏着四个很重要的设计决策:

  1. 什么时候终止循环?
  • LLM 输出的结果显示表示"完成了"(stop_reason = "end_turn"),这也是最常用的判断方式。
  • 循环达到了最大的运行步数了, 起到保护的作用,避免陷入了死循环一直卡住。
  • 执行超时了,工程实现上一般会设置一个超时时间,超过这个时间就返回给用户。
  • 没有 Token 了,这也是很容易出现的问题,这种时候可能得考虑一下上下文管理是否有优化的空间了。
  1. 每次循环应该做多少事情
  • 单工具模式:每次循环都调用一个工具,简单可控。
  • 并行工具模式:一步调用多个工具,效率更高,Claude/GPT 都支持
  1. 执行过程中失败了要怎么办
  • 立即终止:安全但体验差
  • 错误回注 + LLM 自愈(推荐):把错误信息放回消息列表,让 LLM 再跑一次看看
  • 自动重试:对瞬时错误有效,比如网络超时、抖动的情况
  1. 什么时候要用户介入
  • 全自动:不需要人类介入,适合后台任务
  • 关键节点确认(推荐):危险操作前暂停,Claude Code 的命令确认就是这个
  • 每步确认:最安全但最慢

这些都应该是可配置的 ,好的 Harness 把它们暴露为参数,比如 Claude Code 就能让我们选择各种执行模式,还能通过 claude --dangerously-skip-permissions 跳过当前会话的所有权限检查,让其自动执行。

机制二:输出路由(Output Routing)

该机制主要用来回答一个问题:LLM 这次输出的到底是什么意图? 我们要怎么解析 LLM 输出的结果,进而判断模型要我们接下来做什么呢?

在以前是使用 ReAct 模式,LLM 输出的结果是纯文本,我们通过 prompt 教模型要是输出怎么样的数据格式,然后我们自己用正则从纯文本中"扣"出工具调用,这种实现方式又脆弱又丑陋。例如:

  1. 先在 system Prompt 里教模型用特定格式输出,你可能在 system prompt 中这么写:
yaml 复制代码
当你需要调用工具时,请用以下格式:
Thought: 我需要搜索一下天气信息
Action: search
Action Input: {"query": "北京天气"}

当你有最终回答时,请用:
Thought: 我已经获得了所有需要的信息
Final Answer: 北京今天晴天,25°C
  1. 然后 LLM 的输出是这样的纯文本
makefile 复制代码
Thought: 用户想知道北京天气,我需要搜索一下。
Action: search
Action Input: {"query": "东京今天天气"}
  1. 而我们在 Harness 中拿到这段文本,就要用正则去"扣"出来 ActionAction Input 分别是什么了:
python 复制代码
import re

match = re.search(r"Action:\s*(.+)", response)
input_match = re.search(r"Action Input:\s*(.+)", response)

if match:
    tool_name = match.group(1).strip() // 调用对应的 tool 
    args = json.loads(input_match.group(1))  # 很容易崩!

上面 LLM 输出的纯文本可能会存在各种各样的问题:多了一个空格、少了一个引号、把 Action 写成 action 等等,假如正则一解析不出来就有问题了,要么就在 Router 中写大量的兼容逻辑,但是根本枚举不完各种可能存在的错误情况。

而现在各大模型使用的是一种叫 Function Calling 的模式,我明年再调用大模型的 API 接口的时候声明工具的 schema,告诉模型我们现在有哪些工具可用,让模型通过结构化的数据告诉我们需要调用哪些工具,直接输出结构化的 tool_use block,让解析从"猜测"变成了"读取"。

  1. 在模型 API 调用中声明工具 schema
ini 复制代码
response = client.messages.create(
    model="claude-sonnet-4-20250514",
    messages=[{"role": "user", "content": "东京今天天气怎么样"}],
    tools=[
        {
            "name": "search", // 明确声明有这个 tool
            "description": "搜索网络信息",
            "input_schema": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索关键词"}
                },
                "required": ["query"]
            }
        }
    ]
)
  1. 模型返回的也是结构化的数据,不是纯文本:
css 复制代码
[    {        "type": "text",        "text": "让我帮你查一下东京的天气。"    },    {        "type": "tool_use",        "id": "toolu_01A09q90qw90lq917835lq",        "name": "search",        "input": {"query": "东京今天天气"}    }]
  1. 这个时候的 Router 的工具解析代码就很简洁了
ini 复制代码
# 不需要正则!直接按类型过滤
tool_calls = [
    block for block in response.content 
    if block.type == "tool_use"
]

if tool_calls:
    # 结构化的,名称和参数都是确定的
    name = tool_calls[0].name      # "search"
    args = tool_calls[0].input     # {"query": "东京今天天气"}

这种结果是模型训练出来的结果,由模型直接在内部输出结构化的 Json,不需要我们通过 prompt 进行约束,通过参数本身来进行约束,进而可以精确的匹配你声明的工具,在工程的 Router 代码上基本不需要进行任何的容错。

那么模型是怎么知道要触发 Function Calling 模式的呢?答案就是在我们调用 API 传入 tools 参数的时候,模型就会自动进入 Function Calling 模式。

模型输出这里有个重要的参数是 Router 最重要的判断依据,那就是 stop_reason

  • tool_use:模型的执行暂停了,等待我们将工具调用结果再喂给模型。Router 的判断就是要继续执行循环了
  • end_turn:模型认为任务已经完成了。Router 的判断就是要退出循环了

Function calling 的思想是行业标准,但 JSON 结构各家略有不同:

  • Claude:response.content 中有 type: "tool_use" 的 block
  • GPT-4:response.choices[0].message.tool_calls,arguments 是字符串需 JSON.parse
  • Gemini:response.candidates[0].content.parts 中有 functionCall

输出路由层需要做一层适配------这也是 LangChain 等框架存在的原因之一。

虽然 Function Calling 减少了格式错误,但模型偶尔会调用不存在的工具、传入不合理的参数、对危险操作缺乏审慎。验证层从"格式纠错"进化为"语义审查"。

这层验证本身就是一种 Architectural Constraint------用确定性代码防止 Agent 做出危险的工具调用。

另外调用模型传入的 API 列表也不应该全传,工具太多会导致模型选择困难,浪费 Token,增加延迟的,可以适当的增加一些任务动态筛选机制。

总的来说输出路由机制是用来解析模型的输出,判断循环下一步是否要调用工具,是否要退出。而现在有了 function calling 机制让解析模型输出发生了一个质变:

  • 解析从"猜测"变成了"读取"
  • 参数从"祈祷"变成了"约束"
  • 工具选择从 prompt "教学"变成了"内置"

机制三:上下文管理(Context Management)

这是 Context Engineering 在 Agent 运行时的具体实现。管理三层信息:

  1. 第一层:持久化上下文信息

即 system prompt,system message 这部分信息永远都在、永远不变,会包含身份、规则、工具定义等,例如 AGENTS.md/CLAUDE.md 内容。

  1. 第二层:工作上下文

即对话历史,包含 user/ assistant/ tool message,这部分一直累积后就会膨胀起来,需要做各种策略的管理。

  1. 第三层:外部上下文注入

这部分不在消息列表中,但是可以按需取用,例如 Claude.md、向量数据库、完整的 Trace、skill 等知识。 上下文管理的核心职责 = 每次调用 LLM 前,从三层中拼出最优的消息列表。

代码示例

python 复制代码
class ContextManager:  # Context Engineering 的实现
    def build_messages(self) -> list[dict]:
        messages = [{"role": "system", "content": self.system_prompt + self.external_knowledge}]
        for msg in reversed(self.full_history):
            msg = self._maybe_truncate(msg)
            if fits_budget(msg):
                messages.insert(1, msg)
        return messages

class OutputRouter:  # Workflow Control 的实现
    def parse(self, llm_response) -> RouterResult:
        tool_calls = self._extract_tool_calls(llm_response)
        if tool_calls:
            self._validate(tool_calls)  # Architectural Constraint
            return RouterResult(is_final=False, tool_calls=tool_calls)
        return RouterResult(is_final=True, content=self._extract_text(llm_response))

class AgentLoop:  # Workflow Control 的核心
    def run(self, user_input: str) -> str:
        self.context.add_user_message(user_input)
        for step in range(self.max_steps):
            messages = self.context.build_messages() // 加载上下文
            response = self.llm.chat(messages) // 调用大模型
            result = self.router.parse(response) // 解析大模型的结果
            if result.is_final:
                return result.content // 结束
            for tc in result.tool_calls: // 工具调用
                tool_result = self._execute_tool(tc)
                self.context.add_tool_result(tc.id, tool_result)

小结

第二课的核心认知:

  1. 循环控制 是 Workflow Control 的核心------终止条件、并行度、错误处理、人类介入都是可配置的设计决策

  2. 输出路由 把 LLM 的不确定输出转化为确定指令------Function Calling 简化了它,但验证(Architectural Constraint)仍然必要

  3. 上下文管理 是 Context Engineering 的运行时实现------三层信息结构,核心挑战是在有限窗口里放进最相关的信息

下一篇进入最容易翻车的领域:工具调用与错误恢复。

相关推荐
小程故事多_804 小时前
Claude Code常用命令总结
aigc·ai编程
摄影图4 小时前
智能汽车领域应用图素材 汽车AI研发转型
人工智能·科技·aigc
天下无贼!6 小时前
【Python】2026版——FastAPI 框架快速搭建后端服务
开发语言·前端·后端·python·aigc·fastapi
德育处主任8 小时前
『n8n』If组件的用法
人工智能·aigc·工作流引擎
饼干哥哥8 小时前
用龙虾模型把跨境电商的业务SOP转成OpenClaw的Skill
aigc
Code_LT9 小时前
【AIGC】Claude Code 模型配置详解
log4j·aigc
饼干哥哥9 小时前
OpenClaw企业落地的5个进阶配置,价值千亿的市场机会
aigc
92year9 小时前
GTC 2026 + GPT-5.4 实测:推理成本砍到1/10,AI直接操控电脑|周报
aigc
与虾牵手9 小时前
OpenClaw 接入大模型 API 完整教程:2026 Skills 开发从零到跑通
aigc·ai编程