首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >AI Agent 全路径:提示词、工具调用、记忆与多 Agent 协作的四个工程断层

AI Agent 全路径:提示词、工具调用、记忆与多 Agent 协作的四个工程断层

原创
作者头像
用户12339161
发布于 2026-11-10 18:47:29
发布于 2026-11-10 18:47:29
630
举报

Agent 的工程化难点不在单个组件的能力,而在组件之间的契约。提示词决定模型如何理解任务,工具调用把理解转化为行动,记忆让行动跨轮次累积,多 Agent 协作则把单一智能体的能力扩展到分工体系。这四个环节每一个都能单独工作,但组合在一起时,失效以“静默错位”的形式出现:提示词里的约束被工具返回值覆盖,工具执行成功但参数在语义上非法,记忆里的事实和推测混在一起注入下一轮,Agent 之间传递的消息在交接处丢失上下文。下面从四个位置给出可运行的检查代码。

提示词:系统提示不是常量,它的 token 预算会被悄悄挤占

系统提示词通常被当作不可变的字符串拼接进每次请求。问题在于,当上下文组装把检索块、历史对话、工具返回值一起塞进窗口时,系统提示可能被截断,而截断不会报错——模型只是开始表现得“忘了规则”。OpenAI 的 prompt caching 机制也加剧了这个问题的隐蔽性:只要前缀有任何一个字符变化,整个缓存失效,成本和延迟同时上升,但调用本身仍然成功。

工程上的第一个检查点是在组装上下文之前,先量化系统提示的 token 占用并锁定前缀:

代码语言:javascript
复制
import tiktoken

ENC = tiktoken.get_encoding("cl100k_base")

def build_prompt(system: str, few_shot: str, dynamic: str,
                 model_limit: int = 8192, max_output: int = 1024) -> dict:
    static = system + "\n" + few_shot          # 可缓存前缀
    static_tokens = len(ENC.encode(static))
    budget = model_limit - max_output - static_tokens

    dyn_tokens = len(ENC.encode(dynamic))
    if dyn_tokens > budget:
        return {
            "ok": False,
            "error": "dynamic_exceeds_budget",
            "static_tokens": static_tokens,
            "dynamic_tokens": dyn_tokens,
            "budget": budget,
            "truncated_by": dyn_tokens - budget,
        }
    return {"ok": True, "prompt": static + "\n" + dynamic,
            "cacheable_prefix_tokens": static_tokens}

truncated_by 字段是关键:它让“动态部分超预算”从一次静默的模型行为漂移,变成一个可以在日志里聚合的指标。cacheable_prefix_tokens 则提醒开发者,静态部分越长,缓存命中的收益越大,但它的前提是静态部分逐字节不变——任何日期、UUID、会话 ID 混进前缀,缓存就废了。

工具调用:重试风暴比调用失败更危险

工具返回错误时,常见的框架行为是让模型“看到错误后重试”。这个机制在偶发故障下是有效的,但它有一个失效模式:模型用完全相同的参数重试,而工具因为同一个原因持续失败,循环在步数上限内耗尽预算。更糟的是,如果工具是有副作用的(发通知、扣库存),重试可能产生重复执行。

正确的重试应该由执行层控制,而不是交给模型判断。一个带错误分类和幂等键的执行包装器:

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

RETRYABLE = {"timeout", "rate_limit", "transient_io"}

def execute(name: str, args: dict, fn, max_retries: int = 2) -> dict:
    key = hashlib.sha256(f"{name}:{sorted(args.items())}".encode()).hexdigest()[:16]
    attempts = 0
    while attempts <= max_retries:
        try:
            result = fn(**args)
            return {"ok": True, "result": result, "attempts": attempts + 1,
                    "idem_key": key}
        except Exception as e:
            err = classify(e)          # 返回 timeout / rate_limit / ...
            attempts += 1
            if err not in RETRYABLE or attempts > max_retries:
                return {"ok": False, "error": err, "detail": str(e),
                        "attempts": attempts, "idem_key": key}
            time.sleep(min(2 ** attempts * 0.2, 2.0))
    return {"ok": False, "error": "exhausted", "idem_key": key}

idem_key 由工具名和排序后的参数决定,它同时解决了两个问题:重试时下游可以按 key 去重,避免重复副作用;日志里可以用 key 聚合“同一逻辑操作被调用了几次”。classify 把异常映射到错误类别,RETRYABLE 集合决定了哪些错误值得重试。参数不变的重试应该被限制次数,而不是被无限允许——模型没有“这个错误重试也不会好”的判断力,执行层必须有。

记忆:检索相关性不等于时间有效性

Agent 的记忆系统通常用向量检索找“最相关的历史片段”。但相关性高的片段可能来自很久以前,而业务规则已经变了。一个用户三个月前说“预算上限 5000”,今天说“预算上限 8000”,向量检索可能把两条都召回,模型看到两个冲突的数字,倾向于选更显眼的那条。

工程上的缓解是在记忆条目上同时记录时间戳和有效区间,检索时按时间做衰减或显式过滤:

代码语言:javascript
复制
import time

def retrieve_memory(query_vec, store: list, top_k: int = 5,
                    half_life_days: float = 30) -> list:
    scored = []
    now = time.time()
    for item in store:
        sim = cosine(query_vec, item["vec"])
        age_days = (now - item["ts"]) / 86400
        decay = 0.5 ** (age_days / half_life_days)
        scored.append({
            "content": item["content"],
            "score": sim * decay,
            "age_days": round(age_days, 1),
            "superseded": item.get("superseded_by"),
        })
    fresh = [s for s in scored if not s["superseded"]]
    return sorted(fresh, key=lambda x: x["score"], reverse=True)[:top_k]

superseded_by 字段需要在写入新记忆时显式维护:当用户更新了某个事实,旧条目被标记为“已被替代”而不是删除。删除会丢失审计线索,保留但不标记会让冲突内容同时被召回。记忆的工程问题不是“记住多少”,而是“冲突时信哪个”,superseded_by 就是这个决策的落点。

多 Agent 协作:交接消息必须包含完整的任务契约

多 Agent 系统的经典失效是“交接丢上下文”:规划 Agent 把子任务交给执行 Agent 时,只传了一句自然语言描述,执行 Agent 不知道约束条件、输入数据和验收标准,只能自由发挥。任务完成得“看起来像那么回事”,但和上游期望的不一致。

正确的交接应该传递一个结构化的任务契约,而不是一段描述:

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

class TaskHandoff(BaseModel):
    task_id: str
    objective: str                      # 要达成什么
    constraints: list[str]              # 不能违反的规则
    inputs: dict                        # 必需的输入数据
    acceptance: list[str]               # 验收标准,逐条可检查
    deadline_sec: float
    return_schema: dict                 # 下游必须按此结构返回

def delegate(handoff: TaskHandoff, executor) -> dict:
    missing = [k for k in handoff.inputs if handoff.inputs[k] is None]
    if missing:
        return {"ok": False, "error": "incomplete_handoff",
                "missing_inputs": missing}
    result = executor(handoff)
    failed = [a for a in handoff.acceptance
              if not check_acceptance(a, result)]
    return {"ok": not failed, "failed_criteria": failed,
            "result": result}

acceptance 的每一条必须是可被程序检查的,而不是“质量要好”这种描述。incomplete_handoff 检查在委托之前执行,防止下游 Agent 在缺少关键输入时自行猜测。return_schema 让上游可以校验返回值结构,而不是解析自由文本。多 Agent 协作的可靠性,取决于每次交接是否携带了足够让下游独立完成任务的完整信息——自然语言描述适合给人看,结构化契约才适合给 Agent 执行。

这四个断层的共同逻辑是:Agent 全路径上的每一个环节都在把上一环的输出当作自己的输入,而输出的“合法”不等于“完整”。提示词要检查 token 预算和可缓存前缀,工具调用要控制重试次数和幂等性,记忆要区分相关性和时效性,交接要传递结构化契约而非自然语言。每一层都加一道可执行的检查,Agent 才能从“能跑”走到“可交付”。

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

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

目录
  • 提示词:系统提示不是常量,它的 token 预算会被悄悄挤占
  • 工具调用:重试风暴比调用失败更危险
  • 记忆:检索相关性不等于时间有效性
  • 多 Agent 协作:交接消息必须包含完整的任务契约
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档