把 Agent 调优当成搜索问题:微软的"Agent 优化循环"思路

2026-06-03 15 预计阅读时间:1 分钟
来源:oschina.net AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:17 分钟

团队给 Agent 加了评估指标,跑完一看——准确率 72%,工具调用失败率 18%,幻觉率 9%。数字很清楚,下一步却卡住了:改了提示词压幻觉,工具调用失败率反而涨了;换了个工具描述格式,准确率又掉了两分。能测量,但修一个就坏另一个,这是当前 Agent 开发中最让人头疼的循环。

微软的 Vivek Bhaduria、Luis Quintanilla 和 Saket Sathe 团队最近提出了一个解法:别把 Agent 调优当成"手工打磨",把它当成搜索问题。搜索什么?搜索一组 Agent 配置(提示词、工具描述、规划策略、温度参数等),使得它在评估指标上的综合得分最优,且不引入回归。

"测量容易修复难"到底难在哪

先看一个典型场景。假设你有一个客服 Agent,评估维度有三项:

维度 当前得分 目标
意图识别准确率 72% ≥85%
工具调用成功率 82% ≥95%
幻觉率 9% ≤3%

你发现幻觉主要来自"过度推理",于是把提示词从"请详细分析用户问题"改成"仅根据已知信息回答"。幻觉率降到 4%,但意图识别准确率跌到 65%——Agent 变保守了,遇到模糊意图直接放弃。

再试一招:给工具描述加更多示例。工具调用成功率升到 90%,但幻觉率反弹到 7%——Agent 看到丰富的工具示例后,开始"创造性"组合不存在的能力。

这就是核心困境:Agent 的行为由多个耦合因素共同决定,单点修改的副作用不可预测。手工调优本质上是在一个高维空间里做无方向的随机游走。

搜索问题的重新定义

微软团队的思路是:既然单点修改不可靠,那就把整个配置空间显式化,用系统化的搜索策略来探索它。

具体来说,把 Agent 调优建模为:

  • 搜索空间 S:所有可调参数的组合——提示词模板、工具描述格式、系统消息结构、采样参数(temperature、top_p)、规划策略(ReAct vs Plan-and-Execute)、检索策略等
  • 目标函数 f(s):对配置 s 在评估集上的综合得分(可以是加权求和,也可以是约束优化——比如"幻觉率必须 ≤3%,在此约束下最大化准确率")
  • 搜索策略:如何在 S 中高效找到使 f(s) 最优的 s*

这和超参数搜索、prompt optimization 的思路一脉相承,但维度更高、约束更复杂。

优化循环的四步结构

微软提出的"Agent 优化循环"可以拆成四个阶段:

1. 定义评估基准——不是"跑几个例子看看",而是建立可重复的评估流水线:固定测试集、固定评分规则、固定运行环境。每次改动后,全量回归跑一遍。

2. 参数化配置空间——把所有可调因素变成可枚举、可版本化的变量。提示词不再是"一段文字",而是带占位符的模板;工具描述不再是"随便写写",而是有固定 schema 的结构化数据。

3. 执行搜索——根据策略在配置空间中采样,每次采样生成一个候选 Agent 配置,跑评估,记录得分。搜索策略可以是网格搜索、随机搜索、贝叶斯优化,甚至基于 LLM 的元优化(让另一个 LLM 来提议下一组参数)。

4. 回归检测与锁定——找到更优配置后,不是直接上线,而是和当前最佳配置做对比,确认所有维度都不退步,再锁定为新基线。

下面用一个可运行的示例来落地这个思路。

实践:用 Python 搭一个最小 Agent 优化循环

这个示例用 OpenAI API 构建一个简单的问答 Agent,然后在三个评估维度上做约束搜索。你可以直接改造为自己的 Agent 和评估逻辑。

"""
最小 Agent 优化循环示例
依赖: openai>=1.0, pydantic>=2.0
运行前设置: export OPENAI_API_KEY=sk-xxx
"""

import os
import json
import itertools
from dataclasses import dataclass, field
from typing import List, Dict, Tuple
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

# ── 1. 参数化配置空间 ──────────────────────────────────

@dataclass
class AgentConfig:
    """把所有可调因素变成可枚举的变量"""
    system_prompt_template: str       # 系统提示词模板
    temperature: float                # 采样温度
    tool_description_style: str       # 工具描述风格: "minimal" | "detailed" | "example_rich"
    planning_strategy: str            # 规划策略: "direct" | "react" | "plan_then_execute"

# 定义搜索空间:每个维度的候选值
SEARCH_SPACE = {
    "system_prompt_template": [
        "你是一个客服助手,仅根据提供的知识库回答用户问题。如果不确定,请说'我无法确认'。",
        "你是一个客服助手,先判断用户意图,再从知识库中检索答案。回答时引用来源。",
        "你是一个客服助手,按步骤思考:1)识别意图 2)检索信息 3)组织回答。不确定时标注[不确定]。",
    ],
    "temperature": [0.0, 0.3, 0.7],
    "tool_description_style": ["minimal", "detailed", "example_rich"],
    "planning_strategy": ["direct", "react"],
}

def enumerate_configs() -> List[AgentConfig]:
    """网格搜索:枚举所有组合"""
    keys = list(SEARCH_SPACE.keys())
    values = [SEARCH_SPACE[k] for k in keys]
    configs = []
    for combo in itertools.product(*values):
        configs.append(AgentConfig(**dict(zip(keys, combo))))
    return configs

# ── 2. 定义评估基准 ──────────────────────────────────

@dataclass
class EvalResult:
    intent_accuracy: float   # 意图识别准确率
    tool_success_rate: float # 工具调用成功率
    hallucination_rate: float # 幻觉率

# 固定测试集(实际项目中应从文件加载)
EVAL_DATASET = [
    {"query": "我的订单什么时候发货", "expected_intent": "物流查询",
     "expected_answer_contains": "发货", "knowledge_snippet": "标准订单3个工作日内发货"},
    {"query": "退款政策是什么", "expected_intent": "退款咨询",
     "expected_answer_contains": "退款", "knowledge_snippet": "7天内无理由退款"},
    {"query": "你们支持哪些支付方式", "expected_intent": "支付咨询",
     "expected_answer_contains": "支付", "knowledge_snippet": "支持微信支付、支付宝、银行卡"},
    {"query": "我想投诉上次的服务", "expected_intent": "投诉",
     "expected_answer_contains": "投诉", "knowledge_snippet": "投诉请拨打400-123-4567"},
    {"query": "帮我查一下航班信息", "expected_intent": "航班查询",
     "expected_answer_contains": None, "knowledge_snippet": None},  # 超范围问题
]

def evaluate_config(config: AgentConfig) -> EvalResult:
    """对单个配置跑全量评估"""
    intent_correct = 0
    tool_success = 0
    hallucination = 0
    total = len(EVAL_DATASET)

    for item in EVAL_DATASET:
        # 构造完整系统消息
        system_msg = config.system_prompt_template
        if item["knowledge_snippet"]:
            system_msg += f"\n\n知识库片段:{item['knowledge_snippet']}"

        # 构造工具描述(简化示意)
        tool_desc = _build_tool_description(config.tool_description_style)

        # 构造用户消息(根据规划策略调整)
        user_msg = _format_user_query(item["query"], config.planning_strategy)

        # 调用 LLM
        response = client.chat.completions.create(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": system_msg + "\n\n可用工具:" + tool_desc},
                {"role": "user", "content": user_msg},
            ],
            temperature=config.temperature,
            max_tokens=300,
        )
        answer = response.choices[0].message.content

        # 评分逻辑(实际项目中应更精细)
        if item["expected_intent"]:
            # 简化:检查回答是否包含预期关键词
            if item["expected_answer_contains"] and item["expected_answer_contains"] in answer:
                intent_correct += 1
                tool_success += 1
            elif item["expected_answer_contains"] is None:
                # 超范围问题,正确做法是拒绝
                if "无法" in answer or "不确定" in answer or "不支持" in answer:
                    intent_correct += 1
                    hallucination += 0
                else:
                    hallucination += 1
            else:
                hallucination += 1
        else:
            if "无法" in answer or "不确定" in answer:
                intent_correct += 1
            else:
                hallucination += 1

    return EvalResult(
        intent_accuracy=intent_correct / total,
        tool_success_rate=tool_success / total,
        hallucination_rate=hallucination / total,
    )

def _build_tool_description(style: str) -> str:
    if style == "minimal":
        return "search_knowledge_base(query: str) -> str: 搜索知识库"
    elif style == "detailed":
        return ("search_knowledge_base(query: str) -> str: 搜索内部知识库,"
                "返回匹配的文档片段。query应为具体问题关键词。")
    else:  # example_rich
        return ("search_knowledge_base(query: str) -> str: 搜索内部知识库。\n"
                "示例: search_knowledge_base('退款政策') -> '7天内无理由退款'\n"
                "示例: search_knowledge_base('发货时间') -> '3个工作日内发货'")

def _format_user_query(query: str, strategy: str) -> str:
    if strategy == "direct":
        return query
    elif strategy == "react":
        return f"问题:{query}\n请按 Thought -> Action -> Observation -> Answer 的格式回答。"
    else:
        return query

# ── 3. 执行搜索 + 回归检测 ──────────────────────────────

@dataclass
class SearchState:
    best_config: AgentConfig = None
    best_result: EvalResult = None
    best_score: float = -1.0
    history: List[Dict] = field(default_factory=list)

def compute_score(result: EvalResult, 
                  hallucination_threshold: float = 0.05) -> float:
    """约束优化:幻觉率超过阈值则得分为0,否则加权求和"""
    if result.hallucination_rate > hallucination_threshold:
        return 0.0  # 硬约束:幻觉率必须 ≤5%
    return 0.5 * result.intent_accuracy + 0.3 * result.tool_success_rate

def check_regression(new_result: EvalResult, 
                     old_result: EvalResult) -> bool:
    """回归检测:任何维度退步超过5%则判定为回归"""
    if old_result is None:
        return False
    if new_result.intent_accuracy < old_result.intent_accuracy - 0.05:
        return True
    if new_result.tool_success_rate < old_result.tool_success_rate - 0.05:
        return True
    if new_result.hallucination_rate > old_result.hallucination_rate + 0.03:
        return True
    return False

def run_optimization_loop() -> SearchState:
    state = SearchState()
    configs = enumerate_configs()
    print(f"搜索空间大小: {len(configs)} 个配置")

    for i, config in enumerate(configs):
        result = evaluate_config(config)
        score = compute_score(result)

        state.history.append({
            "iteration": i,
            "config": str(config),
            "result": result,
            "score": score,
        })

        # 只有得分更高且无回归才更新最佳配置
        if score > state.best_score and not check_regression(result, state.best_result):
            print(f"✅ 新最佳配置 (iter {i}): score={score:.3f}")
            print(f"   意图准确率={result.intent_accuracy:.2f} "
                  f"工具成功率={result.tool_success_rate:.2f} "
                  f"幻觉率={result.hallucination_rate:.2f}")
            state.best_config = config
            state.best_result = result
            state.best_score = score
        elif score > state.best_score:
            print(f"⚠️  得分更高但有回归 (iter {i}): score={score:.3f},跳过")
        else:
            print(f"    iter {i}: score={score:.3f},未超越当前最佳")

    return state

# ── 4. 运行 ──────────────────────────────────────────

if __name__ == "__main__":
    state = run_optimization_loop()
    print("\n" + "="*60)
    print("优化结果:")
    print(f"最佳得分: {state.best_score:.3f}")
    print(f"最佳配置: {state.best_config}")
    print(f"最佳评估结果: {state.best_result}")

运行前确保设置了 OPENAI_API_KEY。这个示例做了几件关键的事:

  • 配置空间显式化:提示词模板、温度、工具描述风格、规划策略全部枚举,不再是"改改试试"
  • 硬约束+软优化:幻觉率超过 5% 直接得零分,不会出现"总分高但幻觉飙升"的情况
  • 回归检测:新配置即使总分更高,如果任何单项退步超过阈值,也不会被采纳

实际项目中搜索空间远比这大,网格搜索不现实。可以替换 enumerate_configs() 为随机采样或贝叶斯优化:

import random

def random_sample_configs(n: int = 20) -> List[AgentConfig]:
    """随机搜索:采样 n 个配置"""
    configs = []
    for _ in range(n):
        configs.append(AgentConfig(
            system_prompt_template=random.choice(SEARCH_SPACE["system_prompt_template"]),
            temperature=random.choice(SEARCH_SPACE["temperature"]),
            tool_description_style=random.choice(SEARCH_SPACE["tool_description_style"]),
            planning_strategy=random.choice(SEARCH_SPACE["planning_strategy"]),
        ))
    return configs

几个容易踩的坑

评估集太小或太偏。 5 条测试数据只能做演示,真实项目至少 50-100 条,覆盖边界场景和对抗样本。评估集本身也要版本化管理——每次优化循环锁定评估集版本,避免"改题目来凑分数"。

搜索空间定义太粗或太细。 太粗(只调温度和一段提示词)搜不出差异;太细(每个措辞变体都算一个维度)组合爆炸。微软团队的建议是:先粗搜定位大方向,再在最优区域附近细搜。这和超参数搜索的"先随机搜索再局部精调"策略一致。

忽略成本约束。 每次评估都要调 LLM API,网格搜索 3×3×3×2=54 个配置,每个跑 5 条测试,就是 270 次调用。用 gpt-4o 跑一遍可能花几十美元。实际操作中应先用便宜模型(gpt-4o-mini)做粗筛,再用强模型做精调验证。

回归阈值设得太松或太紧。 5% 的退步容忍度是示例值,实际要看业务容忍度。客服场景幻觉率退步 3% 可能就不可接受,但内部工具场景可以更宽松。阈值应该和业务方一起定,而不是工程师拍脑袋。

什么时候该用这个思路

不是所有 Agent 都需要这么重的优化流程。判断标准:

  • Agent 已上线且有稳定评估流水线 → 适合。你已经有数据,只是修不好。
  • Agent 还在原型阶段 → 不适合。评估集都不稳定,搜索结果没有参考价值。
  • 只有 1-2 个可调维度 → 不适合。手工调比搜更快。
  • 5 个以上耦合维度,且改动经常互相打架 → 正是搜索思路的用武之地。

微软团队把这个框架开源在了 GitHub 上(项目名 agent-optimization-loop),提供了更完整的评估流水线模板和搜索策略实现。上面的示例是简化版,核心逻辑一致:把"凭感觉改提示词"变成"在配置空间里做约束搜索"

这个思路的深层启示是——Agent 的行为不是提示词的线性函数,而是多个因素的非线性组合。面对非线性系统,系统化搜索比直觉调试可靠得多。


相关推荐