← 返回教程库

Agent 的 Hello World——做一个会查资料并写报告的 Agent

最后更新 2026-06-25
你将学到
  • 理解 Agent 和 Chatbot 的真正分水岭——会自己拆多步、先查再写
  • 跑通一个完整的"查资料→整理→写报告"Agent,亲眼看到它多步规划的过程
  • 掌握工具循环里"查资料工具"的接入方式,包括真实 API 接入路径
  • 在此基础上进阶:多轮查询、报告分段写、处理搜索失败

很多人的第一个 Agent 只会闲聊——你问它什么,它直接回答,和 Chatbot 没有本质区别。真正的 Hello World 不是这样的:它应该接到一个主题,自己决定去查资料,看完资料再决定要不要继续查,最后把信息整理成一份结构化报告交给你。

这才是 Agent 和 Chatbot 的真正分水岭——会自己拆多步。Chatbot 是一问一答,Agent 是"收到任务→拆步骤→执行→看结果→调整→交付"。这个循环里有真正的自主性。

这节就是来写这个更完整的 Hello World。我们不用重框架,直接用 Anthropic Python SDK 裸写,带你看清楚每一步在代码里对应什么。

如果你还没跑过 用 Claude Agent SDK 写出你的第一个能跑的 Agent(2.3 节),建议先看那节——本节假设你已经装好 SDK、配好 API key。


这个项目要做成什么

给 Agent 一个主题,比如"给我写一份关于大语言模型在医疗影像诊断中应用现状的简报"。

它应该:

  1. 先查资料:调用搜索工具,根据主题生成搜索词、拿到搜索结果
  2. 看结果,决定要不要继续查:如果第一次结果不够,自己再查一次(多轮查询)
  3. 整理成结构化报告:有标题、摘要、主要发现、结论的规范格式
  4. 交付:把报告文本返回给你

整个过程你不需要介入。这就是为什么它叫 Agent 而不是助手。


环境准备

如果已经装好 SDK 可以跳过这里。

# 确认装好
pip install anthropic
python -c "import anthropic; print(anthropic.__version__)"

API key 配进环境变量:

# macOS / Linux
export ANTHROPIC_API_KEY="sk-ant-..."

# Windows PowerShell
$env:ANTHROPIC_API_KEY = "sk-ant-..."

本节代码不需要额外依赖,只用标准库和 anthropic。搜索工具部分提供本地模拟函数,让你能直接跑通——接入真实搜索 API 的替换点会明确标出。


完整可跑代码

下面这段代码可以直接复制粘贴运行。

import anthropic
import json
import re

# ────────────────────────────────────────────────
# 1. 定义工具:搜索 + 读文件(本节主角)
# ────────────────────────────────────────────────
tools = [
    {
        "name": "web_search",
        "description": (
            "根据关键词搜索相关资料,返回搜索结果摘要。"
            "当需要获取某个主题的背景信息、现状、数据、案例时使用此工具。"
            "可以多次调用以搜索不同关键词。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "搜索关键词,建议简洁、精准,例如 '大语言模型 医疗影像诊断 2024'"
                },
                "max_results": {
                    "type": "integer",
                    "description": "最多返回几条结果,默认 3",
                    "default": 3
                }
            },
            "required": ["query"]
        }
    },
    {
        "name": "write_report",
        "description": (
            "把整理好的内容写成一份结构化报告并保存。"
            "当资料收集足够、准备产出最终报告时调用此工具。"
            "报告应包含:标题、执行摘要、主要发现(分点)、结论。"
        ),
        "input_schema": {
            "type": "object",
            "properties": {
                "title": {
                    "type": "string",
                    "description": "报告标题"
                },
                "summary": {
                    "type": "string",
                    "description": "执行摘要,2-3 句话概括核心内容"
                },
                "findings": {
                    "type": "array",
                    "items": {"type": "string"},
                    "description": "主要发现,每条一句话,建议 3-6 条"
                },
                "conclusion": {
                    "type": "string",
                    "description": "结论与展望,1-2 段"
                }
            },
            "required": ["title", "summary", "findings", "conclusion"]
        }
    }
]


# ────────────────────────────────────────────────
# 2. 工具实现:模拟搜索 + 报告写入
# ────────────────────────────────────────────────

def web_search(query: str, max_results: int = 3) -> str:
    """
    【本地模拟版本】
    实际项目里把这里替换成真实搜索 API 调用:
    - Tavily Search API:https://docs.tavily.com/
    - Serper API:https://serper.dev/
    - Brave Search API:https://api.search.brave.com/
    - Bing Search API(Azure 认知服务)
    接入方式以各平台官方文档为准(截稿 2026-06)。

    替换示例(以 Tavily 为例):
        from tavily import TavilyClient
        client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
        results = client.search(query, max_results=max_results)
        return json.dumps(results["results"], ensure_ascii=False, indent=2)
    """
    print(f"  [搜索] 关键词: {query!r}(max_results={max_results})")

    # 本地模拟:根据关键词返回结构化假数据,让工具循环跑通
    mock_data = {
        "default": [
            {
                "title": f"关于"{query}"的研究综述",
                "url": "https://example.com/paper1",
                "snippet": f"本文综述了{query}领域的最新进展,涵盖核心技术路线、主要挑战和应用场景。研究表明,该领域在近年来取得了显著进展,多家机构已发布实用产品。"
            },
            {
                "title": f"{query}:现状与展望",
                "url": "https://example.com/report2",
                "snippet": f"行业报告指出,{query}已在多个垂直场景落地验证,准确率和效率指标明显优于传统方案,但数据合规和模型可解释性仍是主要瓶颈。"
            },
            {
                "title": f"{query}技术对比与最佳实践",
                "url": "https://example.com/best-practice3",
                "snippet": f"从工程落地角度看,{query}的实施需重点考虑数据质量、推理延迟和成本控制三个维度,本文给出了详细的选型建议和典型架构图。"
            }
        ]
    }

    results = mock_data.get("default", [])[:max_results]
    output_lines = [f"搜索关键词:{query}\n找到 {len(results)} 条结果:\n"]
    for i, r in enumerate(results, 1):
        output_lines.append(
            f"{i}. 【{r['title']}】\n"
            f"   来源:{r['url']}\n"
            f"   摘要:{r['snippet']}\n"
        )
    return "\n".join(output_lines)


# 存储最终报告(在真实项目里可以写文件、存数据库)
_report_storage: dict = {}

def write_report(title: str, summary: str, findings: list, conclusion: str) -> str:
    """把整理好的报告保存下来,返回确认信息。"""
    _report_storage["last"] = {
        "title": title,
        "summary": summary,
        "findings": findings,
        "conclusion": conclusion
    }
    print(f"  [写报告] 标题: {title!r}")
    print(f"  [写报告] 发现条数: {len(findings)}")
    return f"报告《{title}》已成功写入,共 {len(findings)} 条主要发现。"


TOOL_FUNCTIONS = {
    "web_search": web_search,
    "write_report": write_report,
}


# ────────────────────────────────────────────────
# 3. Agent 主循环:接任务 → 查资料 → 写报告
# ────────────────────────────────────────────────

def run_research_agent(topic: str, max_rounds: int = 10) -> dict:
    """
    接收一个主题,让 Agent 自己决定:
      - 查哪些关键词(可以多轮)
      - 什么时候觉得资料够了
      - 写出结构化报告
    返回最终报告字典。
    """
    client = anthropic.Anthropic()

    system_prompt = (
        "你是一个专业的研究助手。当收到研究主题时,你应该:\n"
        "1. 先用 web_search 搜索相关资料(可以搜索 1-3 次,用不同关键词覆盖不同角度)\n"
        "2. 分析搜索结果,判断是否需要补充查询\n"
        "3. 资料足够后,用 write_report 写出结构化报告\n"
        "注意:报告的 findings 必须基于搜索到的实际内容,不要凭空编造数据。"
    )

    messages = [
        {"role": "user", "content": f"请帮我研究以下主题并写一份报告:{topic}"}
    ]

    print(f"\n{'='*60}")
    print(f"研究主题:{topic}")
    print(f"{'='*60}\n")

    round_count = 0

    while round_count < max_rounds:
        round_count += 1
        print(f"[第 {round_count} 轮] 模型思考中...")

        response = client.messages.create(
            model="claude-opus-4-5",   # 以官方文档为准(截稿 2026-06)
            max_tokens=2048,
            system=system_prompt,
            tools=tools,
            messages=messages,
        )

        print(f"[第 {round_count} 轮] stop_reason = {response.stop_reason}")

        # 任务完成
        if response.stop_reason == "end_turn":
            # 检查是否已经写了报告
            if "last" in _report_storage:
                report = _report_storage["last"]
                print("\n✓ 报告已生成!")
                _print_report(report)
                return report
            else:
                # 模型直接回答了但没用工具写报告,把文字内容包装成报告
                text = "".join(
                    b.text for b in response.content if hasattr(b, "text")
                )
                print(f"\n模型直接回复(未调 write_report):{text[:200]}...")
                return {"raw_response": text}

        # 模型要调工具
        if response.stop_reason == "tool_use":
            messages.append({"role": "assistant", "content": response.content})

            tool_results = []
            for block in response.content:
                if block.type == "tool_use":
                    tool_name = block.name
                    tool_input = block.input
                    print(f"  → 调用工具: {tool_name}")

                    if tool_name in TOOL_FUNCTIONS:
                        try:
                            result = TOOL_FUNCTIONS[tool_name](**tool_input)
                        except Exception as e:
                            result = f"工具执行出错:{e}"
                            print(f"  ✗ 工具出错: {e}")
                    else:
                        result = f"工具 '{tool_name}' 未找到"

                    print(f"  ← 工具返回: {str(result)[:100]}...")
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })

            messages.append({"role": "user", "content": tool_results})

        else:
            print(f"未预期的 stop_reason: {response.stop_reason}")
            break

    print(f"已达最大轮次 {max_rounds},强制退出")
    return _report_storage.get("last", {})


def _print_report(report: dict):
    """格式化打印报告。"""
    print(f"\n{'─'*60}")
    print(f"报告标题:{report.get('title', '')}")
    print(f"{'─'*60}")
    print(f"\n【执行摘要】\n{report.get('summary', '')}")
    print(f"\n【主要发现】")
    for i, f in enumerate(report.get("findings", []), 1):
        print(f"  {i}. {f}")
    print(f"\n【结论】\n{report.get('conclusion', '')}")
    print(f"{'─'*60}\n")


# ────────────────────────────────────────────────
# 4. 跑起来
# ────────────────────────────────────────────────
if __name__ == "__main__":
    topic = "大语言模型在医疗影像诊断中的应用现状"
    report = run_research_agent(topic)
    print("最终返回对象 keys:", list(report.keys()))

你应该看到什么

跑通后,终端应该打印出类似下面这样的过程(因为用的是模拟搜索,每次结果一样,但流程是真实的):

============================================================
研究主题:大语言模型在医疗影像诊断中的应用现状
============================================================

[第 1 轮] 模型思考中...
[第 1 轮] stop_reason = tool_use
  → 调用工具: web_search
  [搜索] 关键词: '大语言模型 医疗影像诊断 应用现状'(max_results=3)
  ← 工具返回: 搜索关键词:大语言模型 医疗影像诊断 ...

[第 2 轮] 模型思考中...
[第 2 轮] stop_reason = tool_use
  → 调用工具: web_search
  [搜索] 关键词: '医疗 AI 影像识别 挑战 数据合规'(max_results=3)
  ← 工具返回: 搜索关键词:医疗 AI 影像识别 ...

[第 3 轮] 模型思考中...
[第 3 轮] stop_reason = tool_use
  → 调用工具: write_report
  [写报告] 标题: '大语言模型在医疗影像诊断中的应用现状报告'
  [写报告] 发现条数: 5
  ← 工具返回: 报告《...》已成功写入,共 5 条主要发现。

[第 4 轮] 模型思考中...
[第 4 轮] stop_reason = end_turn

✓ 报告已生成!
────────────────────────────────────────────────────────────
报告标题:大语言模型在医疗影像诊断中的应用现状报告
────────────────────────────────────────────────────────────

【执行摘要】
大语言模型……(摘要内容)

【主要发现】
  1. ……
  2. ……
  ...

注意这个顺序tool_use(搜索)→ tool_use(再搜索)→ tool_use(写报告)→ end_turn。这就是 Agent 自己规划的多步过程——它没有等你告诉它"现在可以写报告了",而是自己判断"资料够了,该写了"。

这正是 AI Agent 是什么(1.1 节)里讲的自主性:模型自己决定用哪个工具、用几次、什么时候停


Agent 怎么"自己多步规划"

很多人读到这里会有一个问题:它为什么要查两次?谁告诉它的?

答案是:没有人告诉它,它自己判断的。原理是这样的:

每一轮,模型收到的是完整的对话历史(包括之前所有的搜索结果)。它在看完第一次搜索结果后,做了一个判断:

"这些结果覆盖了技术层面,但还缺合规和挑战角度,我应该再搜一次。"

这个判断完全在模型内部完成,没有任何外部规则触发。这和简单的 if-else 流程不一样——它是模型在理解上下文之后的语义推理。

这就是为什么工具的 description 写好非常重要:"可以多次调用以搜索不同关键词" 这句话,给了模型"我可以搜多次"的明确许可。如果去掉这句话,模型可能只搜一次就开始写报告。

口诀:description 是模型的行动许可证,写好它比写提示词魔法更管用。


进阶:多轮查询与分段写报告

让报告分段生成

现在的 write_report 一次性提交整个报告。如果主题很复杂、信息量大,可以改成分段提交:

# 在 tools 里加一个"追加章节"工具
{
    "name": "append_section",
    "description": "向正在撰写的报告中追加一个章节,在资料分批整理时使用",
    "input_schema": {
        "type": "object",
        "properties": {
            "section_title": {"type": "string", "description": "章节标题"},
            "content": {"type": "string", "description": "章节正文"},
            "order": {"type": "integer", "description": "章节序号,从 1 开始"}
        },
        "required": ["section_title", "content", "order"]
    }
}

这样 Agent 可以先写摘要章节、再写发现章节、最后写结论章节,每个章节背后都是一次独立的工具调用。这种模式下报告可以随时中止、重启,只要历史记录在就行。

控制多轮搜索深度

如果你不想让 Agent 无限搜索,可以在 system_prompt 里加一条限制:

system_prompt = (
    "……\n"
    "搜索次数控制在 2-4 次,不要过度搜索。"
    "如果前两次搜索结果已经足够支撑报告,直接进入写报告阶段。"
)

也可以在工具的 description 里加:"最多调用 3 次,避免冗余搜索"。

接入真实搜索 API

上面代码里 web_search 是本地模拟函数。要接入真实搜索,只需替换函数体,接口签名不变:

def web_search(query: str, max_results: int = 3) -> str:
    # 以 Tavily 为例(以官方文档为准,截稿 2026-06)
    # pip install tavily-python
    from tavily import TavilyClient
    import os
    client = TavilyClient(api_key=os.environ["TAVILY_API_KEY"])
    response = client.search(query, max_results=max_results)
    results = response.get("results", [])
    lines = [f"搜索关键词:{query}\n找到 {len(results)} 条结果:\n"]
    for i, r in enumerate(results, 1):
        lines.append(
            f"{i}. 【{r['title']}】\n"
            f"   来源:{r['url']}\n"
            f"   摘要:{r['content'][:200]}\n"
        )
    return "\n".join(lines)

工具的 namedescriptioninput_schema 不用改,Agent 主循环不用改,只需换掉实现函数。这也是为什么"工具 = 描述 + 实现"这个分离结构在代码里更好维护。

关于 Skills 和 MCP 的区别——把搜索工具做成 MCP Server,就可以跨多个 Agent 复用,不用每次复制粘贴函数实现,这是更规范的扩展方式,后续进阶节会讲。


故障排查表

症状 原因 解法
anthropic.AuthenticationError API key 未配或失效 确认 echo $ANTHROPIC_API_KEY 能打印出非空值;到 Console 检查 key 状态
stop_reason 一直是 tool_use,报告一直没生成 模型的搜索循环卡住,或 write_report 的 description 不够清晰 检查 write_report 的 description 是否明确说明"资料收集完毕后调用";加 max_rounds 保底退出(代码里已有)
tool_use_id 匹配失败,出现 invalid_request_error tool_result 里的 tool_use_id 没有对应上 block.id 检查 tool_results 列表里每条的 tool_use_id 是否直接用的 block.id,不要手动构造
报告内容全是假数据 / 模拟搜索结果 没有接入真实搜索 API 正常,本节模拟版本就是这样;要真实内容,替换 web_search 函数体,接入 Tavily/Serper 等(以官方文档为准)
_report_storage 没有 last key,函数返回空 dict 模型没有调 write_report 就返回了 end_turn system_prompt 里加强"必须使用 write_report 工具来提交报告,不要直接回复文字";或检查 write_report 的 description 是否说清了调用时机
搜索只调了一次就开始写报告,报告内容单薄 description 里没有给"可以多次搜索"的许可 web_search 的 description 里加上"可以多次调用以覆盖不同角度";或在 system_prompt 里说明"搜索 2-3 次"

常见问题

Q:我换了主题,Agent 总是第一次搜索就直接写报告,深度不够怎么办?

两个地方调整:一是在 system_prompt 里写明"需要搜索至少 2 次,从不同角度覆盖";二是在 web_search 的 description 里加上"建议用不同关键词搜索 2-3 次,以获得多角度信息"。模型会根据这些提示来决定搜索次数。工具的 description 对行为影响比 system prompt 更直接。

Q:这个 Agent 和简单的 RAG(检索增强生成)有什么区别?

RAG 的检索是固定流程:用户问 → 系统检索 → 塞进 context → 模型回答,这个流程是你写死的,模型不控制检索步骤。这个 Agent 里,模型自己决定搜什么词、搜几次、什么时候够了——检索本身成了模型行动的一部分。这才是 Agent 的核心:模型对自己的行动有自主性。

Q:报告里的内容全是假数据,能当作真实调研用吗?

不能。本节的 web_search 是本地模拟,返回的是占位文字,只是让工具循环跑通。要生产可用的报告,必须接入真实搜索 API(Tavily、Serper、Bing Search 等,以官方文档为准,截稿 2026-06),拿到真实搜索结果后,报告内容才有意义。

Q:搜索结果太长,塞进 messages 后上下文爆了怎么办?

两个应对方向:第一,在 web_search 返回时就截断,比如每条 snippet 只取前 300 字;第二,引入摘要步骤,每次搜索后先让模型提炼要点存到一个"笔记"结构里,而不是把全文塞进 messages。后者是更规范的 Agent 记忆管理方案,AI Agent 阶梯 的后续节会展开讲。


小结

  • Agent 和 Chatbot 的真正分水岭不是能不能用工具,而是会不会自己拆多步——先查、判断够不够、再查、最后交付,全程不需要你介入。
  • 这个报告 Agent 的结构就是:tools(搜索 + 写报告)+ 一个 while 循环 + messages 历史——几十行代码,完整的自主多步流程。
  • description 写好是关键:它决定模型什么时候调工具、调几次、什么时候停。description 写给模型看,不是给人看。
  • 接入真实搜索 API 只需替换 web_search 函数体,其余代码不变——这是工具"描述与实现分离"带来的好处。

接下来可以看 用 Claude Agent SDK 写出你的第一个能跑的 Agent(2.3 节)对照一下结构差异,或者去看 Skills vs MCP:两种扩展方式,了解怎么让这个搜索工具在多个 Agent 之间复用。更多阶梯内容在 AI Agent 智能体落地指南 里。

👉 看看 AI 数字员工落地指南,或了解 数字员工搭建实战课。需要为企业落地方案,欢迎找我们聊 企业服务

📄 来源 / 自校链接

本文为学习整理,关键步骤与代码请结合下列官方来源验证。

内容有错、看不懂、或想看下一期?告诉我们 →

本文为学习与落地整理,AI 工具与平台更新较快,关键步骤请结合官方最新资料验证。见免责声明