首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >智能体开发中的三个静默失效:工具参数、上下文压缩与执行预算

智能体开发中的三个静默失效:工具参数、上下文压缩与执行预算

原创
作者头像
资源大佬 jzit-top
发布于 2026-11-09 18:22:21
发布于 2026-11-09 18:22:21
550
举报

智能体与普通 LLM 应用的分界线在于“会行动”:普通应用输出一段文字,智能体调用工具、修改状态、在循环中迭代。这个差别放大了静默失效的风险——一个对话应用答错了,影响止于这一轮对话;一个智能体把错误参数传进了真实工具,影响会落到数据库、发件箱和下游系统里。更麻烦的是,这三类失效都不以异常的形式出现:工具调用“成功返回”了,上下文压缩“正常执行”了,智能体循环“跑完了”,但工程意图在行为层面已经偏离。

断层一:工具参数是 LLM 的“意图”,不是可执行契约

几乎所有智能体框架的工具调用路径都遵循同一个模式:把工具的 JSON Schema 交给模型,模型返回一个 tool_calls 数组,框架解析后执行。问题出在“解析后执行”这一步——模型返回的参数在结构上看起来符合 schema,但语义上不保证合法。一个 send_email 工具声明 to: string,模型可能返回 "to": "张三",schema 校验通过,邮件客户端拒绝这个地址,异常在工具内部抛出,而智能体的重试逻辑又用同样的参数再试一次。

工具层的正确设计是把模型输出当作不可信输入,在执行前做一次独立的参数校验,校验失败时返回结构化的错误给模型,而不是把异常抛进工具实现:

代码语言:javascript
复制
from pydantic import BaseModel, EmailStr, ValidationError

class SendEmailArgs(BaseModel):
    to: EmailStr
    subject: str
    body: str

def execute_tool(name: str, raw_args: dict, registry: dict) -> dict:
    spec = registry.get(name)
    if spec is None:
        return {"ok": False, "error": f"unknown_tool:{name}"}
    try:
        args = spec.args_model(**raw_args)
    except ValidationError as e:
        return {"ok": False, "error": "invalid_args",
                "detail": e.errors(), "retryable": True}
    try:
        result = spec.fn(**args.model_dump())
        return {"ok": True, "result": result}
    except Exception as e:
        return {"ok": False, "error": "tool_failed",
                "detail": str(e), "retryable": spec.retryable}

两个关键字段值得注意:retryable 让框架区分“参数错了,重试有意义”和“工具故障,重试会放大影响”;detail 把 Pydantic 的字段级错误原样回传给模型,模型在下一轮能看到“to 不是合法邮箱”,而不是一个笼统的“调用失败”。把校验错误变成模型可读的反馈,是让智能体自我修正的前提;把校验错误变成一个被吞掉的异常,是让智能体在同一个错误上循环的前提。

断层二:上下文压缩是“摘要”,不是“无损存储”

长对话的智能体必须做上下文压缩,否则 token 预算会在几十轮内耗尽。常见做法是把较早的消息摘要成一段自然语言,保留最近 N 轮原文。这个机制在大多数轮次里工作正常,但它有一个安静的失效模式:摘要会丢掉后续步骤需要的精确细节,而智能体不知道自己丢了。

例如,用户在第三轮说“订单号是 A12345,退款金额 300 元,不要通知客户”,二十轮之后摘要变成“用户申请了一笔退款”。智能体在调用 create_refund 时拿到的是一个没有订单号、没有金额、没有“不通知”约束的上下文。它不会报错,它会基于不完整的信息自信地生成参数。

工程上的缓解不是“不要压缩”,而是把压缩拆成两层:自然语言摘要用于叙述性历史,结构化状态用于事实性信息。关键实体(订单号、金额、用户明确表达的约束)不进入摘要,而是写入一个独立的、每次调用都注入的 state 对象:

代码语言:javascript
复制
import json

def build_context(history: list, summary: str, state: dict) -> list:
    state_block = (
        "以下是从对话中提取的已确认事实,必须严格遵守:\n"
        + json.dumps(state, ensure_ascii=False, indent=2)
    )
    messages = [{"role": "system", "content": state_block}]
    if summary:
        messages.append({"role": "system",
                         "content": f"此前对话摘要:{summary}"})
    messages.extend(history[-6:])
    return messages

# state 由每轮结束后的抽取步骤更新,不参与摘要
state = {"order_id": "A12345", "refund_amount": 300,
         "notify_customer": False}

state 的更新时机也有讲究:它应该在工具调用成功之后更新,而不是在模型声称“已完成”时更新。模型说“我已经退款了”不等于工具返回了成功,把模型的自我陈述写进 state,等于把幻觉固化成了后续所有轮次的事实前提。

断层三:没有预算约束的循环,成本是静默的

智能体的自主性来自循环:思考、调工具、观察结果、再思考。没有显式预算的循环有两个失效方向。一个是步数失控:工具持续返回“部分成功”,模型持续重试,循环在框架的默认上限内耗尽了整轮 token 预算,最终返回一个半成品,而账单已经产生。另一个是非幂等重试:send_email 或 create_order 在网络超时后由框架自动重试,第一次调用其实已经成功,第二次调用产生了重复记录。

两类问题都可以用一个执行包装器来约束,把“步数、成本、幂等键”变成显式参数:

代码语言:javascript
复制
import hashlib, time

class AgentBudget:
    def __init__(self, max_steps=12, max_cost_usd=0.50):
        self.max_steps = max_steps
        self.max_cost_usd = max_cost_usd
        self.steps = 0
        self.cost = 0.0
        self.seen_actions = set()

    def check(self, action: dict, cost_delta: float) -> dict:
        self.steps += 1
        self.cost += cost_delta
        if self.steps > self.max_steps:
            return {"stop": True, "reason": "step_budget_exceeded"}
        if self.cost > self.max_cost_usd:
            return {"stop": True, "reason": "cost_budget_exceeded"}
        if action.get("side_effect"):
            key = hashlib.sha256(
                f"{action['tool']}:{action['args']}".encode()
            ).hexdigest()[:16]
            if key in self.seen_actions:
                return {"stop": True, "reason": "duplicate_side_effect"}
            self.seen_actions.add(key)
        return {"stop": False}

side_effect 是一个由工具声明决定的标记,duplicate_side_effect 的判定基于“工具名 + 参数”的哈希。这不是一个完美的幂等性方案——两次意图不同但参数相同的调用会被误判——但它把“重复发送”从一个只能在事后从日志里发现的问题,变成了一个在调用前就被拦截的显式决策。真正需要精确幂等的场景,应该把幂等键下沉到工具实现里,由下游系统按 key 去重。

这三个断层的共同逻辑是:智能体的每一步都在把模型的输出转化为系统状态,而模型输出在结构上合法、在语义上不保证正确。工具参数要用独立于 schema 的运行时校验拦一道,上下文压缩要把事实性信息从叙述性摘要里分离出来,循环要把步数、成本和副作用重复放进显式的预算对象里。这三件事都不改变智能体的“智能”,但它们决定了智能体的行为是在“能跑”的层面停留,还是在“可控”的层面成立。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 断层一:工具参数是 LLM 的“意图”,不是可执行契约
  • 断层二:上下文压缩是“摘要”,不是“无损存储”
  • 断层三:没有预算约束的循环,成本是静默的
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档