深度研究 Agent 落地生产的实战教训

2026-05-27 29 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

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

预计阅读时间:13 分钟

当 LLM 从"单轮问答"进化到"多步推理+多跳检索+结构化输出",Deep Research Agent 就不再是玩具,而是真正能替人完成调研工作的系统。Thoughtworks 的 Sarang Kulkarni 在 Arc of AI 2026 大会上分享了他们在生产环境中部署多 Agent 深度研究系统的经验——这些教训值得每一个正在搭建 Agent pipeline 的团队认真看。

深度研究 Agent 到底在做什么

普通 RAG 是"一问一答一检索",Deep Research Agent 则更像一个研究员的工作流:拿到一个复杂问题后,先拆解子问题,再逐个检索、交叉验证、补充遗漏,最后把所有发现组织成一份结构化分析报告。核心能力有三层:

  • 动态推理:不是写死流程,而是 Agent 根据中间结果决定下一步走哪条路。
  • 多跳检索:第一轮检索的结果可能触发第二轮、第三轮检索,信息是逐层展开的。
  • 结构化输出:最终产出不是一段自由文本,而是有章节、有引用、有结论的分析报告。

这三层叠加起来,系统的复杂度会急剧上升——这也是生产落地最难的地方。

多 Agent 架构:拆职责比堆参数更重要

Kulkarni 的核心观点之一是:不要试图让一个"超级 Agent"包办所有事。生产级的研究系统应该把职责拆开,每个 Agent 只做一件事,做到可靠。

典型拆法:

Agent 职责 关键约束
Planner 拆解研究问题为子任务列表 输出必须是可枚举的子问题,不能模糊
Searcher 对单个子问题执行多跳检索 每跳必须带来源 URL,最多 3 跳
Synthesizer 合并多个 Searcher 的结果 必须标注信息冲突和缺失
Reviewer 检查报告的逻辑完整性和引用准确性 只输出 pass/fail + 具体问题清单

拆职责的好处是:每个 Agent 的 prompt 更短、行为更可预测、失败更容易定位。坏处是:Agent 间通信和状态管理变复杂了——这正是下一节要解决的问题。

状态与通信:生产系统的隐形杀手

多 Agent 系统在 demo 里跑得漂亮,上线后最先出问题的往往是状态管理。Kulkarni 提到了几个高频故障点:

  1. 上下文窗口溢出:Searcher 返回的原始网页内容塞进 Synthesizer 的上下文,直接超限。解法是 Searcher 必须做摘要再传递,而不是原样转发。
  2. 中间结果丢失:Planner 生成了 8 个子问题,Searcher 只完成了 6 个就超时了,Synthesizer 不知道哪些没做。解法是用一个共享状态存储(哪怕就是 JSON 文件)记录每个子任务的状态。
  3. 循环调用:Reviewer 发现问题后让 Planner 重新拆解,Planner 又生成类似子问题,Searcher 又走一遍——无限循环。解法是设置最大迭代次数,并在 Reviewer 反馈中要求给出具体修改方向,而不是笼统的"再研究一下"。

下面是一个用 Python 实现的最小可运行多 Agent 研究框架,展示了状态管理和 Agent 通信的核心模式:

import json
import os
from datetime import datetime
from typing import Literal

from openai import OpenAI

client = OpenAI()  # 默认读取 OPENAI_API_KEY 环境变量

STATE_FILE = "research_state.json"
MAX_ITERATIONS = 3

# ── 状态管理 ──────────────────────────────────────────────

def load_state() -> dict:
    if os.path.exists(STATE_FILE):
        with open(STATE_FILE) as f:
            return json.load(f)
    return {"sub_questions": [], "results": {}, "iteration": 0, "status": "planning"}

def save_state(state: dict):
    with open(STATE_FILE, "w") as f:
        json.dump(state, f, indent=2, ensure_ascii=False)

# ── Agent 定义 ────────────────────────────────────────────

def planner_agent(question: str, feedback: str | None = None) -> list[str]:
    """拆解研究问题为子问题列表"""
    prompt = f"""你是一个研究规划师。把以下研究问题拆解为 3-5 个具体可检索的子问题。
只输出 JSON 数组,不要其他内容。

研究问题:{question}
"""
    if feedback:
        prompt += f"\n上一轮审阅反馈(请据此调整子问题):{feedback}"

    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )
    raw = json.loads(resp.choices[0].message.content)
    # 期望 {"sub_questions": ["q1", "q2", ...]}
    return raw.get("sub_questions", [])

def searcher_agent(sub_question: str) -> dict:
    """对单个子问题执行模拟检索与摘要"""
    prompt = f"""你是一个信息检索与摘要专家。针对以下子问题,
模拟多跳检索过程,输出:
1. findings: 你找到的关键信息(2-3 条)
2. sources: 信息来源(URL 或文档名)
3. confidence: 高/中/低
4. gaps: 还有什么信息缺失

只输出 JSON,不要其他内容。

子问题:{sub_question}
"""
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )
    return json.loads(resp.choices[0].message.content)

def reviewer_agent(question: str, report: str) -> dict:
    """审阅报告完整性,输出 pass/fail + 具体反馈"""
    prompt = f"""你是一个研究审阅者。检查以下报告是否完整回答了原始问题。
输出 JSON:
- decision: "pass" 或 "fail"
- feedback: 如果 fail,给出具体缺失方向(不要笼统说"再研究一下")

原始问题:{question}

报告:
{report}
"""
    resp = client.chat.completions.create(
        model="gpt-4o",
        messages=[{"role": "user", "content": prompt}],
        response_format={"type": "json_object"},
    )
    return json.loads(resp.choices[0].message.content)

# ── 主循环 ────────────────────────────────────────────────

def run_research(question: str) -> str:
    state = load_state()
    feedback = None

    for i in range(MAX_ITERATIONS):
        state["iteration"] = i + 1
        print(f"\n=== 第 {i+1} 轮迭代 ===")

        # 1. 规划
        sub_questions = planner_agent(question, feedback)
        state["sub_questions"] = sub_questions
        print(f"子问题:{sub_questions}")

        # 2. 检索(每个子问题一个 searcher)
        results = {}
        for sq in sub_questions:
            result = searcher_agent(sq)
            results[sq] = result
            print(f"  [{sq}] → confidence={result.get('confidence')}, gaps={result.get('gaps')}")
        state["results"] = results

        # 3. 合成报告
        findings_text = ""
        for sq, r in results.items():
            findings_text += f"\n## {sq}\n"
            for f in r.get("findings", []):
                findings_text += f"- {f}\n"
            findings_text += f"来源: {r.get('sources', [])}\n"
            findings_text += f"置信度: {r.get('confidence')}\n"
            if r.get("gaps"):
                findings_text += f"信息缺口: {r['gaps']}\n"

        report = f"# 研究报告:{question}\n{findings_text}"
        state["report"] = report

        # 4. 审阅
        review = reviewer_agent(question, report)
        decision = review.get("decision", "fail")
        print(f"审阅结果:{decision}")

        save_state(state)

        if decision == "pass":
            print("✅ 报告通过审阅")
            return report
        else:
            feedback = review.get("feedback", "")
            print(f"❌ 需要改进:{feedback}")

    print("⚠️ 达到最大迭代次数,返回当前报告")
    return state.get("report", "未能生成完整报告")

# ── 运行 ──────────────────────────────────────────────────

if __name__ == "__main__":
    question = "2024-2025年主流大模型厂商的推理能力对比,包括价格、延迟和准确率"
    final_report = run_research(question)
    print("\n" + final_report)

运行前确保已设置 OPENAI_API_KEY 环境变量,并安装 openai 包(pip install openai)。这个框架虽然用了模拟检索而非真实搜索引擎,但状态管理、迭代控制、Agent 通信的模式和 Kulkarni 描述的生产系统是一致的。你可以把 searcher_agent 替换为真实搜索 API(如 Tavily、SerpAPI)来升级。

检索层的坑:多跳不是多搜一遍

多跳检索听起来简单——搜不到就再搜一次。但生产环境里,多跳的真正含义是根据上一跳的结果决定下一跳的策略

几个实操要点:

  • 第一跳用宽查询,目的是找到信息地图;第二跳用窄查询,针对具体缺口深挖。
  • 每跳必须记录来源。没有来源的发现等于没有发现——Synthesizer 无法验证,Reviewer 无法审计。
  • 设置跳数上限。Kulkarni 建议最多 3 跳,超过 3 跳通常意味着第一跳的查询方向就错了,应该回到 Planner 重新拆解,而不是继续在错误方向上深挖。
  • 去重与冲突检测。不同子问题的检索结果可能引用同一来源但说法不同,Synthesizer 必须显式标注冲突,而不是悄悄选一个。

上线前的检查清单

把 Kulkarni 的教训浓缩成一份可操作的清单:

检查项 为什么重要 常见遗漏
每个 Agent 的 prompt 是否短于 500 词 长 prompt 导致行为不可预测 把所有指令塞给一个 Agent
Agent 间传递的是摘要还是原文 原文会撑爆上下文窗口 直接把搜索结果原文传给合成 Agent
是否有共享状态存储 否则中间结果丢失后无法恢复 只靠内存传递
是否设置了最大迭代次数 否则 Reviewer 反馈可能触发无限循环 忘记加循环保护
检索结果是否带来源 URL 没有来源就无法审计和验证 只返回内容不返回出处
多跳检索是否有跳数上限 否则会在错误方向上越走越远 每跳都是独立查询,没有策略递进
Reviewer 反馈是否具体 "再研究一下"等于没反馈 Reviewer 只说 fail 不说哪里 fail
是否有成本监控 多 Agent 多轮调用,token 消耗是乘法级 只测功能不看账单

最后一点容易被忽略:成本是乘法级增长的。一个研究问题拆成 4 个子问题,每个子问题 3 跳检索,每跳调一次 LLM,一轮迭代就是 12+ 次调用。如果 Reviewer 打回两轮,就是 36+ 次。在上线前,必须建立每次研究任务的 token 消耗基线,否则账单会让你比系统 bug 更早发现问题。

Deep Research Agent 不是更聪明的聊天机器人,而是一个需要工程纪律的分布式系统。把职责拆清、把状态管好、把成本盯住——这三件事做对了,Agent 才能从 demo 走到生产。


相关推荐