第 71 章

案例:多步骤深度研究 Agent

第七十一章:案例:多步骤深度研究 Agent

章节导语

信息时代最大的悖论是:数据越来越多,而真正的洞察越来越难得。一份高质量的竞品分析报告,过去需要分析师花费数天收集、阅读、归纳、交叉验证;而今天,这项工作可以由一个 Hermes Agent 在数十分钟内完成——它能自主搜索学术论文、新闻报道、技术文档,逐页阅读,提炼关键信息,追踪引用来源,最终输出一份结构完整、引用可追溯的研究报告。本章将从零构建这个多步骤深度研究 Agent,重点讲解它如何在保证质量的前提下自动化整个研究流程。


71.1 需求分析:自动化研究报告的核心挑战

研究报告的五大难点

手工研究流程的时间分布:
搜索相关资料          ██████ 30%
阅读筛选内容          ████████ 40%
交叉验证事实          ████ 20%
整理撰写报告          ██ 10%

自动化研究面临的核心挑战:

挑战 描述 难度
来源可靠性 如何区分权威来源和低质量内容
事实核查 同一事实在不同来源可能矛盾
深度 vs 广度 过于广泛则浅显,过于聚焦则遗漏
引用追踪 每个论断必须有可追溯的来源
内容去重 同一信息被多个来源重复报道

Agent 的目标功能


71.2 系统架构

研究流程状态机

┌─────────────────────────────────────────────────────────┐
│                   Research Agent Pipeline                │
│                                                         │
│  [INIT] 解析研究主题,制定搜索策略                       │
│     ↓                                                   │
│  [SEARCH] 多维度搜索(Tavily/SerpAPI)                  │
│     ↓                                                   │
│  [FILTER] 评估来源可靠性,过滤低质量结果                 │
│     ↓                                                   │
│  [READ] 深度阅读高价值页面(获取全文)                   │
│     ↓                                                   │
│  [EXTRACT] 从每个来源提炼关键信息                        │
│     ↓                                                   │
│  [VERIFY] 交叉核验:寻找印证或矛盾                       │
│     ↓                                                   │
│  [SYNTHESIZE] 综合所有信息,识别主要论点                 │
│     ↓                                                   │
│  [WRITE] 生成结构化研究报告                             │
│     ↓                                                   │
│  [QUALITY_CHECK] 验证引用完整性,标注置信度              │
└─────────────────────────────────────────────────────────┘

架构组件

┌─────────────────────────────────────────────────────────┐
│                  Deep Research Agent                     │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │               Hermes LLM Core                   │   │
│  │  - 研究策略规划                                  │   │
│  │  - 信息提炼与综合                                │   │
│  │  - 报告撰写                                      │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  ┌──────────────┐  ┌──────────────┐  ┌─────────────┐  │
│  │  搜索工具    │  │  阅读工具    │  │  写作工具   │  │
│  │              │  │              │  │             │  │
│  │ - Tavily API │  │ - URL Fetch  │  │ - Markdown  │  │
│  │ - SerpAPI    │  │ - PDF解析    │  │ - 引用格式化│  │
│  │ - arXiv API  │  │ - 内容清洗   │  │ - 目录生成  │  │
│  └──────────────┘  └──────────────┘  └─────────────┘  │
│                                                         │
│  ┌─────────────────────────────────────────────────┐   │
│  │           Research Memory(研究记忆)            │   │
│  │  - 已访问 URL 列表(防重复)                     │   │
│  │  - 提炼的关键事实库                              │   │
│  │  - 引用追踪表                                    │   │
│  └─────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

工具集设计

工具 描述 API/库
search_web 通用网络搜索 Tavily API
search_academic 学术论文搜索 arXiv / Semantic Scholar
fetch_page_content 获取页面全文 requests + BeautifulSoup
extract_pdf_text 提取 PDF 文本 pdfplumber
extract_key_facts 从文本提炼事实 Hermes LLM
cross_verify_fact 交叉核验事实 Hermes LLM
write_report_section 写报告章节 Hermes LLM
format_citations 格式化引用列表 自定义

71.3 完整实现代码

核心 Agent

# research_agent/agent.py
import os
import json
from datetime import datetime
from openai import OpenAI

client = OpenAI(
    base_url=os.getenv("HERMES_BASE_URL", "http://localhost:11434/v1"),
    api_key=os.getenv("HERMES_API_KEY", "ollama"),
)
MODEL = os.getenv("HERMES_MODEL", "nous-hermes-2-mixtral-8x7b-dpo")

SYSTEM_PROMPT = """你是一位专业的研究分析师,擅长从海量信息中提炼洞察,撰写高质量研究报告。

你的工作方法:
1. **广泛搜索**:从多个角度搜索,不只是表面问题
2. **深度阅读**:对关键来源进行全文阅读,而非仅看摘要
3. **交叉核验**:对重要论断,寻找至少2个独立来源印证
4. **引用追踪**:每个数据点都记录来源,不允许无来源的论断
5. **结构化输出**:报告必须有清晰的层级结构

置信度标注规则:
- 🟢 高置信度:2+ 个权威来源印证
- 🟡 中置信度:1个可靠来源,未能交叉验证
- 🔴 低置信度:仅有推测或单一来源,需标注

报告格式:
- 执行摘要(300字内)
- 主要发现(3-5个)
- 详细分析(分章节)
- 结论与建议
- 参考文献"""

TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "search_web",
            "description": "使用 Tavily 搜索引擎搜索网络内容",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索查询词"},
                    "max_results": {"type": "integer", "default": 5, "description": "最多返回结果数"},
                    "include_domains": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "优先搜索的域名,如 ['arxiv.org', 'nature.com']"
                    },
                    "search_depth": {
                        "type": "string",
                        "enum": ["basic", "advanced"],
                        "default": "advanced"
                    }
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "search_academic",
            "description": "搜索学术论文和研究报告",
            "parameters": {
                "type": "object",
                "properties": {
                    "query": {"type": "string"},
                    "year_from": {"type": "integer", "description": "发表年份下限"},
                    "max_results": {"type": "integer", "default": 5}
                },
                "required": ["query"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "fetch_page_content",
            "description": "获取指定 URL 的页面全文内容",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "目标页面 URL"},
                    "max_chars": {"type": "integer", "default": 8000, "description": "最大字符数"}
                },
                "required": ["url"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "record_fact",
            "description": "记录一个已核验的事实到研究记忆库",
            "parameters": {
                "type": "object",
                "properties": {
                    "fact": {"type": "string", "description": "事实陈述"},
                    "source_url": {"type": "string"},
                    "source_title": {"type": "string"},
                    "confidence": {
                        "type": "string",
                        "enum": ["high", "medium", "low"]
                    },
                    "category": {"type": "string", "description": "事实分类,如 '市场规模'/'技术现状'"}
                },
                "required": ["fact", "source_url", "confidence", "category"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_recorded_facts",
            "description": "获取已记录的所有研究事实",
            "parameters": {
                "type": "object",
                "properties": {
                    "category": {"type": "string", "description": "按分类过滤,空则返回全部"}
                }
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "write_final_report",
            "description": "将所有收集的信息整合成最终研究报告",
            "parameters": {
                "type": "object",
                "properties": {
                    "topic": {"type": "string"},
                    "facts": {"type": "array"},
                    "report_structure": {
                        "type": "array",
                        "items": {"type": "string"},
                        "description": "报告章节列表"
                    }
                },
                "required": ["topic", "facts"]
            }
        }
    }
]


class ResearchMemory:
    """研究记忆:存储已访问URL和已提炼事实"""
    
    def __init__(self):
        self.visited_urls: set = set()
        self.facts: list = []
        self.citations: dict = {}  # url -> {title, date, content}
    
    def add_fact(self, fact: str, source_url: str, source_title: str,
                 confidence: str, category: str):
        self.facts.append({
            "id": len(self.facts) + 1,
            "fact": fact,
            "source_url": source_url,
            "source_title": source_title,
            "confidence": confidence,
            "category": category
        })
        return {"success": True, "fact_id": len(self.facts)}
    
    def get_facts(self, category: str = None) -> list:
        if category:
            return [f for f in self.facts if f["category"] == category]
        return self.facts
    
    def mark_visited(self, url: str):
        self.visited_urls.add(url)
    
    def is_visited(self, url: str) -> bool:
        return url in self.visited_urls


def run_research_agent(topic: str, depth: str = "comprehensive") -> dict:
    """运行深度研究 Agent"""
    memory = ResearchMemory()
    
    print(f"[研究Agent] 开始研究主题:{topic}")
    print(f"[研究Agent] 研究深度:{depth}")
    
    messages = [
        {"role": "system", "content": SYSTEM_PROMPT},
        {
            "role": "user",
            "content": f"""请对以下主题进行深度研究,并输出完整的研究报告:

**研究主题:** {topic}
**研究深度:** {depth}(comprehensive=全面综合,focused=聚焦分析)
**当前时间:** {datetime.now().strftime('%Y年%m月')}

研究步骤:
1. 制定搜索策略(至少3个不同角度的搜索词)
2. 执行多轮搜索,收集来自不同来源的信息
3. 深度阅读最相关的5-10个来源的全文
4. 记录关键事实(使用 record_fact 工具)
5. 对重要数据进行交叉验证
6. 调用 write_final_report 生成报告

请开始研究。"""
        }
    ]
    
    max_iterations = 30  # 研究 Agent 需要更多迭代
    
    for iteration in range(max_iterations):
        print(f"[研究Agent] 第 {iteration + 1} 轮推理...")
        
        response = client.chat.completions.create(
            model=MODEL,
            messages=messages,
            tools=TOOLS,
            tool_choice="auto",
            temperature=0.3,  # 研究需要适度创造性
            max_tokens=4000
        )
        
        message = response.choices[0].message
        messages.append(message)
        
        if not message.tool_calls:
            print("[研究Agent] 研究完成")
            return {
                "status": "completed",
                "report": message.content,
                "facts_collected": len(memory.facts),
                "sources_visited": len(memory.visited_urls),
                "iterations": iteration + 1
            }
        
        # 执行工具调用
        for tool_call in message.tool_calls:
            name = tool_call.function.name
            args = json.loads(tool_call.function.arguments)
            print(f"  → 工具调用: {name}")
            
            result = _dispatch_tool(name, args, memory)
            messages.append({
                "role": "tool",
                "tool_call_id": tool_call.id,
                "content": json.dumps(result, ensure_ascii=False)
            })
    
    return {"status": "max_iterations", "facts_collected": len(memory.facts)}


def _dispatch_tool(name: str, args: dict, memory: ResearchMemory) -> dict:
    from .tools import search_tools, content_tools
    
    if name == "search_web":
        return search_tools.search_tavily(**args)
    elif name == "search_academic":
        return search_tools.search_academic(**args)
    elif name == "fetch_page_content":
        url = args["url"]
        if memory.is_visited(url):
            return {"cached": True, "message": "该URL已访问过"}
        memory.mark_visited(url)
        return content_tools.fetch_page(url, args.get("max_chars", 8000))
    elif name == "record_fact":
        return memory.add_fact(**args)
    elif name == "get_recorded_facts":
        return {"facts": memory.get_facts(args.get("category"))}
    elif name == "write_final_report":
        return content_tools.write_report(
            args["topic"], 
            memory.get_facts(),
            args.get("report_structure", [])
        )
    return {"error": f"未知工具: {name}"}

搜索工具实现

# research_agent/tools/search_tools.py
import os
import requests
from typing import List, Optional

TAVILY_API_KEY = os.getenv("TAVILY_API_KEY")

def search_tavily(
    query: str,
    max_results: int = 5,
    include_domains: List[str] = None,
    search_depth: str = "advanced"
) -> dict:
    """Tavily 搜索 API"""
    payload = {
        "api_key": TAVILY_API_KEY,
        "query": query,
        "max_results": max_results,
        "search_depth": search_depth,
        "include_answer": True,   # 获取 AI 摘要
        "include_raw_content": False,
    }
    
    if include_domains:
        payload["include_domains"] = include_domains
    
    resp = requests.post(
        "https://api.tavily.com/search",
        json=payload,
        timeout=30
    )
    resp.raise_for_status()
    data = resp.json()
    
    results = []
    for r in data.get("results", []):
        results.append({
            "title": r["title"],
            "url": r["url"],
            "snippet": r.get("content", "")[:500],
            "score": r.get("score", 0),
            "published_date": r.get("published_date", "")
        })
    
    return {
        "query": query,
        "total": len(results),
        "ai_answer": data.get("answer", ""),
        "results": results
    }


def search_academic(
    query: str,
    year_from: int = None,
    max_results: int = 5
) -> dict:
    """搜索 Semantic Scholar 学术数据库"""
    params = {
        "query": query,
        "limit": max_results,
        "fields": "title,abstract,year,authors,externalIds,citationCount,url"
    }
    if year_from:
        params["year"] = f"{year_from}-"
    
    resp = requests.get(
        "https://api.semanticscholar.org/graph/v1/paper/search",
        params=params,
        timeout=30
    )
    
    if resp.status_code != 200:
        return {"error": f"API错误: {resp.status_code}", "results": []}
    
    papers = []
    for p in resp.json().get("data", []):
        papers.append({
            "title": p.get("title", ""),
            "abstract": p.get("abstract", "")[:400],
            "year": p.get("year"),
            "citation_count": p.get("citationCount", 0),
            "url": p.get("url", ""),
            "authors": [a["name"] for a in p.get("authors", [])[:3]]
        })
    
    # 按引用数排序(引用多的通常更重要)
    papers.sort(key=lambda x: x["citation_count"], reverse=True)
    
    return {"query": query, "total": len(papers), "papers": papers}

内容处理工具

# research_agent/tools/content_tools.py
import re
import requests
from bs4 import BeautifulSoup
from typing import List

def fetch_page(url: str, max_chars: int = 8000) -> dict:
    """获取页面全文内容"""
    headers = {
        "User-Agent": "Mozilla/5.0 (Research Bot; +https://example.com/bot)"
    }
    
    try:
        resp = requests.get(url, headers=headers, timeout=15)
        resp.raise_for_status()
        
        soup = BeautifulSoup(resp.text, "html.parser")
        
        # 移除无用标签
        for tag in soup(["script", "style", "nav", "footer", "aside", "iframe"]):
            tag.decompose()
        
        # 优先提取文章主体
        article = (
            soup.find("article") or 
            soup.find("main") or 
            soup.find(class_=re.compile(r"(content|article|post|entry)")) or
            soup.find("body")
        )
        
        text = article.get_text(separator="\n", strip=True) if article else ""
        
        # 清理多余空行
        text = re.sub(r"\n{3,}", "\n\n", text)
        
        return {
            "url": url,
            "title": soup.title.string if soup.title else "",
            "content": text[:max_chars],
            "total_chars": len(text),
            "truncated": len(text) > max_chars
        }
    
    except requests.RequestException as e:
        return {"url": url, "error": str(e), "content": ""}


def write_report(topic: str, facts: List[dict], structure: List[str]) -> dict:
    """生成最终研究报告的模板"""
    if not structure:
        structure = [
            "执行摘要",
            "研究背景",
            "主要发现",
            "详细分析",
            "结论与建议",
            "参考文献"
        ]
    
    # 按类别整理事实
    facts_by_category = {}
    for fact in facts:
        cat = fact.get("category", "其他")
        facts_by_category.setdefault(cat, []).append(fact)
    
    # 生成引用列表
    citations = []
    seen_urls = set()
    for fact in facts:
        url = fact.get("source_url", "")
        if url and url not in seen_urls:
            seen_urls.add(url)
            citations.append({
                "title": fact.get("source_title", url),
                "url": url
            })
    
    return {
        "topic": topic,
        "total_facts": len(facts),
        "total_sources": len(citations),
        "facts_by_category": facts_by_category,
        "suggested_structure": structure,
        "citations": citations,
        "ready_for_writing": True
    }

71.4 质量控制机制

引用追踪系统

class CitationTracker:
    """引用追踪:确保每个论断都有来源"""
    
    def __init__(self):
        self._citations = {}  # id -> citation
        self._counter = 0
    
    def add(self, url: str, title: str, accessed_date: str = None) -> int:
        """添加引用,返回引用编号"""
        self._counter += 1
        self._citations[self._counter] = {
            "id": self._counter,
            "url": url,
            "title": title,
            "accessed": accessed_date or "2026-04"
        }
        return self._counter
    
    def format_bibliography(self, style: str = "apa") -> str:
        """格式化参考文献列表"""
        lines = ["## 参考文献\n"]
        for cid, c in self._citations.items():
            if style == "apa":
                lines.append(f"[{cid}] {c['title']}. 检索自 {c['url']} ({c['accessed']})")
            else:
                lines.append(f"[{cid}] [{c['title']}]({c['url']})")
        return "\n".join(lines)
    
    def inject_citations(self, text: str) -> str:
        """在报告文本中注入引用标记"""
        # 查找类似 [cite:url] 的占位符,替换为编号引用
        import re
        def replace_cite(match):
            url = match.group(1)
            for cid, c in self._citations.items():
                if c["url"] == url:
                    return f"[{cid}]"
            return "[?]"  # 未知来源
        return re.sub(r"\[cite:(https?://[^\]]+)\]", replace_cite, text)

事实核查流程

def cross_verify_fact(
    claim: str, 
    supporting_sources: list, 
    contradicting_sources: list
) -> dict:
    """对一个论断进行置信度评估"""
    
    confidence_score = 0
    
    # 权威来源加权
    authoritative_domains = [
        "nature.com", "science.org", "arxiv.org", 
        "gov", ".edu", "reuters.com", "bloomberg.com"
    ]
    
    for source in supporting_sources:
        url = source.get("url", "")
        # 权威来源得分更高
        is_authoritative = any(d in url for d in authoritative_domains)
        confidence_score += 2 if is_authoritative else 1
    
    for source in contradicting_sources:
        confidence_score -= 1
    
    if confidence_score >= 3:
        level = "high"
        emoji = "🟢"
    elif confidence_score >= 1:
        level = "medium"
        emoji = "🟡"
    else:
        level = "low"
        emoji = "🔴"
    
    return {
        "claim": claim,
        "confidence": level,
        "confidence_emoji": emoji,
        "score": confidence_score,
        "supporting_count": len(supporting_sources),
        "contradicting_count": len(contradicting_sources),
        "recommendation": (
            "可直接引用" if level == "high" else
            "需标注来源有限" if level == "medium" else
            "建议进一步核实或标注为推测"
        )
    }

71.5 报告输出示例

# AI 大模型市场竞争格局研究报告

**研究日期:** 2026年4月
**数据来源:** 15个网络来源,4篇学术论文
**研究置信度:** 综合评分 82/100

---

## 执行摘要

大型语言模型市场在2025年经历了快速整合,从数十家竞争者收缩至形成三大阵营:
闭源商业模型(OpenAI/Anthropic/Google)、开源社区模型(Meta/MistralAI/NousResearch),
以及专业垂直领域模型。🟢 市场总规模预计2026年达到 1,200亿美元 [1][2]。

## 主要发现

1. **开源追赶效应明显** 🟢:开源模型在代码生成、指令遵循等基准测试中
   已接近或超过2024年初的商业模型水平 [3]

2. **推理成本持续下降** 🟡:主流模型推理成本年均下降约60%,
   部分厂商单独发布"轻量推理版本"以应对成本竞争 [4]

3. **Agent 能力成为新战场** 🟢:2025年下半年,各主要厂商均推出
   针对 Agent 场景优化的模型版本 [5][6]

---

## 参考文献

[1] Goldman Sachs AI Report 2026. https://gs.com/ai-report-2026
[2] Gartner Emerging Tech Hype Cycle 2025. https://gartner.com/...
...

71.6 耗时与成本分析

不同研究深度的资源消耗对比

研究类型 搜索轮次 阅读页面 LLM 调用次数 大约耗时 估算成本
快速概览 2-3 3-5 10-15 3-5 分钟 $0.05-0.15
标准研究 5-8 8-15 20-30 10-20 分钟 $0.20-0.60
深度综合 10-15 15-30 35-50 30-60 分钟 $0.60-2.00
学术级研究 20+ 30-50 60-100 1-3 小时 $2.00-8.00

注:成本基于 Hermes 自托管推理场景,商业 API 成本约为 5-10 倍

成本优化策略

class CostOptimizer:
    """研究成本优化器"""
    
    def __init__(self, budget_usd: float = 1.0):
        self.budget = budget_usd
        self.spent = 0.0
        self.token_cost_per_1k = 0.002  # 自托管估算
    
    def should_continue(self, facts_count: int, min_facts: int = 10) -> bool:
        """判断是否应继续研究"""
        # 已超预算:停止
        if self.spent >= self.budget:
            return False
        # 事实数量已足够:可以停止
        if facts_count >= min_facts and self.spent >= self.budget * 0.5:
            return False
        return True
    
    def should_deep_read(self, relevance_score: float, threshold: float = 0.7) -> bool:
        """判断是否值得深度阅读该页面"""
        return relevance_score >= threshold and self.spent < self.budget * 0.8
    
    def record_tokens(self, input_tokens: int, output_tokens: int):
        cost = (input_tokens + output_tokens) / 1000 * self.token_cost_per_1k
        self.spent += cost

本章小结

本章构建了一个具备生产可用性的深度研究 Agent:

这个 Agent 的核心价值在于将研究员的方法论(系统搜索、严格核验、引用完整)编码为 Agent 的行为模式,而不仅仅是"让 AI 回答问题"。

思考题

  1. 如何检测并处理研究中发现的相互矛盾的事实?
  2. 研究 Agent 如何避免"确认偏误"——只收集支持某个预设结论的信息?
  3. 对于中文研究主题,如何平衡中英文来源的比例和权威性?
  4. 如何评估一份 AI 生成研究报告的质量?可以设计哪些自动化质量指标?
本章评分
4.8  / 5  (3 评分)

💬 留言讨论