首页 / 文章 / 实用提示:Harness工程:裸代理——为何你的框架会如此设计

实用提示:Harness工程:裸代理——为何你的框架会如此设计

《实用笔记:Harness工程框架:裸代理模式——为何你的框架需要合同、校验机制以及供采用该模式的团队使用的即插即用代码模块》的操作指南。

2011 词

可将此内容视为面向操作员的《Harness Engineering: The Naked Agent: Why Your Framework Hands You a Loop, Not a Harness — I》中理念的重构版本:清晰的阶段划分、有序的代码模块以及能够经受交接考验的恢复说明。

第一部分:在真实流量涌入之前,单纯的智能体循环看似功能强大。这就是为什么生产环境中的故障通常源于模型周围缺失的支撑框架,而非模型本身。

第一部分:将基础阶段视为可测量的界面使用效果最佳。在扩大范围之前,先记录一份理想的运行日志、一个故障案例以及回滚说明。同时记录正常流程与恢复流程。重试机制、人工审核环节以及死信处理都是产品的一部分,而非后续需要补充的功能。为每轮对话和每次会话设定token预算。智能体工具会大量消耗上下文资源,设置上限可避免演示过程变成意外的费用账单。

大多数智能体故障并非模型本身的问题,而是其周围缺失的约束机制所致。以下是在 Claude Agent SDK 与 LangChain Deep Agents 中,没有相应约束的 AI 智能体的表现形式,以及它在实际使用场景中会出现故障的三种具体方式。

将智能体故障视为可测量的现象来处理最为有效。在扩大范围之前,先记录一份最佳响应样本、一个故障案例以及回滚说明。 优先选择小型且可测试的单元,而非庞大的脚本。当某个步骤出现故障时,故障应能指向单一责任主体,而非复杂的流程链。 需为每轮对话和每次会话设定token预算。智能体工具往往会大量消耗上下文资源,设置上限可避免演示过程变成意外的费用账单。

模型并非变量

将该模型视为可测量的表面时,其效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。 将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,明确成功标准,绝不允许默默完成部分任务。 为每轮及每次会话设定token预算。智能工具会大量消耗上下文资源;设置上限可避免演示过程变成意外的费用账单。

“裸模型”究竟意味着什么

将“What naked”实际所指的阶段视为可测量的界面来处理效果最佳。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,就能避免在从演示环境过渡到共享环境时出现意外账单。

import anthropic

client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY
TOOLS = [
    {"name": "search_flights",
     "description": "Search flights between two cities for a date.",
     "input_schema": {"type": "object", "properties": {
         "origin": {"type": "string"}, "destination": {"type": "string"},
         "date": {"type": "string", "description": "YYYY-MM-DD"}},
         "required": ["origin", "destination", "date"]}},
    {"name": "book_flight",
     "description": "Book a specific flight.",
     "input_schema": {"type": "object", "properties": {
         "flight_id": {"type": "string"}, "passenger_name": {"type": "string"}},
         "required": ["flight_id", "passenger_name"]}},
]
def run_naked(user_msg: str) -> str:
    messages = [{"role": "user", "content": user_msg}]
    while True:                                   # ① no iteration cap
        resp = client.messages.create(
            model="claude-sonnet-4-6", max_tokens=1024,
            tools=TOOLS, messages=messages,
        )
        if resp.stop_reason != "tool_use":
            return resp.content[0].text
        call = next(b for b in resp.content if b.type == "tool_use")
        result = dispatch(call.name, call.input)
                                             # ② direct side effect, no check
        messages.extend([
                                             # ③ whole history, every turn
            {"role": "assistant", "content": resp.content},
            {"role": "user", "content": [{"type": "tool_result",
                "tool_use_id": call.id, "content": result}]},
        ])

Claude Agent SDK中的裸代理

在测试阶段,将“裸代理”视为可度量的对象使用效果最佳。在扩大范围之前,先记录一份理想的操作日志、一个故障案例以及回滚说明。 应将配置置于应用程序代码之外。环境文件、密钥存储和功能标志应集中存放,以便操作人员无需查看整个系统结构即可进行审计。 需提供具有明确架构和清晰副作用标签的工具。主机在自动批准之前必须知道哪些调用会修改状态。 在测试阶段,将“裸代理”视为可度量的对象使用效果最佳。在扩大范围之前,先记录一份理想的操作日志、一个故障案例以及回滚说明。 相比庞大的脚本,更应优先使用小型且可测试的单元。当某个步骤出现故障时,故障原因应能指向单一责任模块,而非复杂的流程链。

import asyncio
from claude_agent_sdk import (
    query, ClaudeAgentOptions, tool,
    create_sdk_mcp_server, AssistantMessage, ResultMessage,
)

@tool("search_flights", "Search flights between two cities for a date.",
      {"origin": str, "destination": str, "date": str})
async def search_flights(args):
                                              # ① no check that date exists
    hits = flights_api.search(**args)
    return {"content": [{"type": "text", "text": str(hits)}]}
@tool("book_flight", "Book a specific flight.",
      {"flight_id": str, "passenger_name": str})
async def book_flight(args):
                                               # ② destructive, ungated
    confirmation = flights_api.book(**args)
    return {"content": [{"type": "text", "text": confirmation}]}
server = create_sdk_mcp_server("travel", tools=[search_flights, book_flight])
async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"travel": server},
        allowed_tools=["mcp__travel__search_flights",
                       "mcp__travel__book_flight"],
    )
    async for msg in query(prompt="Rebook this customer for March 32nd.",
                           options=options):
        if isinstance(msg, AssistantMessage):
            for b in msg.content:
                if hasattr(b, "text"):
                    print(b.text)
        elif isinstance(msg, ResultMessage):
            print("done:", msg.subtype)
                                              # ③ no state survives this run

LangChain Deep Agents中的“裸代理”

对于“舞台上的裸体特工”这一阶段,在修改代码之前需明确输入参数、该步骤的负责人以及结束标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。 应将此阶段视为输入与经过验证的输出之间的契约。为相关成果命名,定义成功判定标准,并拒绝默许的半完成状态。 在网关处进行身份验证,在数据层面重新授权。仅凭承载令牌并不足以界定租户边界。

from langchain.tools import tool
from deepagents import create_deep_agent

@tool
def search_flights(origin: str, destination: str, date: str) -> str:
    """Search flights between two cities for a date (YYYY-MM-DD)."""
    return str(flights_api.search(origin, destination, date))
                                                # ① no date check
@tool
def book_flight(flight_id: str, passenger_name: str) -> str:
    """Book a specific flight."""
    return flights_api.book(flight_id, passenger_name)
                                                 # ② ungated side effect
agent = create_deep_agent(
                                                 # ③ the loop, no controls
    model="anthropic:claude-sonnet-4-6",
    tools=[search_flights, book_flight],
)
result = agent.invoke({"messages": [{"role": "user",
    "content": "Rebook this customer for March 32nd."}]})
print(result["messages"][-1].content)
# Ask a follow-up in a second invoke, and it starts from zero: no thread,
# no memory.

它会以三种方式出错

在修改代码之前,需将监控流程分为三个阶段:明确输入参数、该步骤的负责人以及退出标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测隐藏状态。除了功能结果外,还需记录执行时间以及令牌或查询成本。提前了解这些成本信息,可避免在流程从演示环境切换到共享环境时出现意外费用。

故障1:格式错误的参数被传递给了具有破坏性的函数

对于“故障1:阶段格式错误”的情况,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 配置信息应置于应用程序代码之外。环境文件、密钥存储以及功能标志应集中存放于一个位置,以便操作人员无需查看整个流程即可进行审计。 在网关处进行身份验证,在数据层进行重新授权。仅凭承载令牌并不足以界定租户边界。 对于“故障1:阶段格式错误”的情况,应在修改代码之前明确输入参数、该步骤的负责人以及终止标准。操作人员应能够从已知的检查点重新运行该步骤,而无需猜测其中的隐藏状态。 相较于庞大的脚本,应优先使用小型且可测试的单元。当某个步骤出现故障时,故障原因应能指向具体的责任模块,而非整个复杂的流程。

book_flight(flight_id=”AC-PHANTOM”, passenger_name=”J. Moffatt”)
# -> “Booked.” The action fired. Nothing in the loop asked whether it should.

故障2:上下文规模激增且质量在无声中下降

在处理故障2导致的上下文规模激增问题时,首先需明确相关契约:所需的输入参数、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 将这一阶段视为输入与验证后输出之间的契约。为相关产物命名,定义成功判定标准,并杜绝无声的部分完成情况。 需记录每次调用的工具名称、参数哈希值、延迟时间以及最终结果。没有这些记录,调试过程将会浪费大量时间。

故障3:工具出现错误,但调试代理却报告成功

在处理“故障3:工具阶段”时,首先写下相关约定:所需输入、成功信号以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。 在功能结果旁记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在代码从演示环境转向共享环境时出现意外费用。 为每次调用记录工具名称、参数哈希值、延迟时间以及最终结果。没有这些记录,调试代理将陷入无休止的循环,耗费大量时间。

每个部分都需遵循的结构

在“明确每个组件的功能”阶段,首先需列出相关规范:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 将配置信息与应用程序代码分开存放。环境文件、密钥存储以及功能开关应集中管理,以便操作人员无需查看整个系统结构即可进行审计。 需为每次调用记录工具名称、参数哈希值、延迟时间以及执行结果。若没有这些记录,调试过程将会浪费大量时间。 在“明确每个组件的功能”阶段,首先需列出相关规范:所需输入、成功标志以及部分失败时的处理方式。这样的清单能确保后续的代码修改保持一致性。 相比庞大的脚本,应优先选择小型且可测试的单元。当某个步骤出现故障时,故障原因应能明确指向某个特定功能,而非整个复杂的流程。

今天就行动

“今日完成”阶段若被视为可度量的目标,效果最佳。在扩大范围之前,先记录一份优秀的处理结果、一个失败案例以及回滚说明。将此阶段视为输入与已验证输出之间的契约,为相关成果命名、明确成功标准,绝不允许默许不完整的完成状态。应使用具有严格结构定义且带有明确副作用标注的工具,让负责人在自动批准之前清楚知晓哪些操作会改变系统状态。

模型部分是最简单的

将该模型视为可测量的界面时,其在测试阶段的表现最为理想。在扩大范围之前,先记录一个成功的案例、一个失败案例以及回滚说明。在功能结果旁同时记录处理时间以及令牌或查询成本。提前了解成本情况,可避免从演示环境过渡到共享环境时出现意外账单。为每轮对话和每次会话设定令牌预算,因为智能工具会大量消耗上下文资源,设置上限能防止演示过程变成意外收费的来源。

操作检查清单

在处理操作检查清单阶段时,首先要明确约定:所需的输入参数、成功标志,以及部分失败时的处理方式。这样的检查清单能确保后续的代码修改保持一致性。

需同时记录正常流程和故障恢复流程。重试机制、人工干预环节以及错误处理方式都是产品本身的组成部分,而非后续需要补充的内容。

记录每次调用的工具名称、参数哈希值、延迟时间以及执行结果。如果没有这些追踪信息,调试代理将陷入无限循环,耗费大量时间。

保持数据结构简洁且类型明确。嵌套的数据结构会掩盖具体是哪个节点设置了哪个字段,还会在中断后导致程序无法继续运行。

只要预算允许,就在持续集成过程中使用测试用例而非真实的付费 API 来对关键路径进行功能测试。

在功能结果旁同时记录执行时间以及令牌或查询成本。提前了解成本情况,可避免在系统从演示环境切换到共享环境时出现意外账单。

在升级技术栈之前,先冻结现有版本,为关键路径生成标准化的操作记录,并确认回滚步骤。共享环境需要设置速率限制、租户验证机制,以及明确的密钥轮换负责人。与其追求华丽的临时演示,不如注重扎实的稳定性。

关于765280e2df21的批处理说明:不要将提供者密钥放入代码仓库,为每个会话设置令牌上限,并将转录内容存储在评估测试用例的旁边,以便后续更换模型时仍能保持可比性。