☰
Google Agent白皮书可运行源码:Python最小骨架与避坑指南
2026/10/10 10:23:23 网站建设 项目流程

简介:这份资源是Google第二部Agent白皮书《Agents Companion》的配套可运行源码包,面向希望将代理技术从原型Demo推进到生产部署的开发者与AI工程实践者。白皮书从概念普及转向工程化落地,围绕技术架构进阶、生态协同与标准构建、企业级落地路径及未来趋势展开,源码则帮助读者把其中的理论与策略直接落到代码层面验证。压缩包共3个文件,包含1个inscode工程配置、1个html页面与1个gitignore忽略规则文件,整体约9KB,体量轻便,适合快速导入学习或二次开发。目前已有136人学习下载。借助这份资料,读者可对照白皮书梳理代理系统的技术路线图与实施框架,理解企业级AI应用在架构设计、生态协作和落地挑战上的关键思路,并基于可运行源码进行测试与改造,为自身项目提供可参考的工程起点。

1. 从一份 Agent 白皮书说起:为什么“能跑起来的源码”比概念更重要

最近技术圈里被反复提到的一个组合词是 Google Agent 白皮书,配套还挂着“可运行源码”几个字。很多人第一反应是:又是一份讲概念的 PDF?但真正做过 Agent 落地的人会关心另一件事——白皮书里描述的那套编排逻辑,到底能不能在本地用几十行代码跑通,跑通之后又能不能换成自己的工具和模型。这才是分水岭。

Agent 这个词被用得很泛。有人拿它指一个会自动调工具的对话机器人,有人拿它指多角色协作的任务流水线,还有人把它等同于“带记忆的 RAG”。白皮书的价值不在于重新定义名词,而在于把一套可复用的结构讲清楚:任务怎么拆、工具怎么注册、状态怎么在步骤之间传递、失败怎么回退。而“可运行源码”的价值,是把这套结构从纸面变成你能改、能调试、能接自己业务的东西。

这篇笔记面向两类人:一类是刚接触 Agent、想找一个最小可运行骨架上手的新手;另一类是已经用过大模型 API、但被“多步任务一长就乱”折磨过的熟手。我会按“先立住结构,再动手复现,最后讲坑”的顺序往下走,所有代码都是可抄作业级别的最小实现,不依赖任何特定厂商的封闭 SDK。

2. Agent 白皮书里的四层结构:拆开看才不玄学

2.1 把 Agent 拆成模型、工具、记忆、编排四件事

不管白皮书用什么词,一个能跑的 Agent 系统基本都能拆成四层。第一层是模型层,负责理解和生成,它本身不会“做事”,只会输出文本或结构化指令。第二层是工具层,是真正产生副作用的环节——查数据库、发请求、读写文件、调计算函数。第三层是记忆层,短期记忆是当前任务的上下文,长期记忆是跨会话可检索的知识。第四层是编排层,决定下一步调谁、传什么参数、什么时候停。

很多人一上来就写一个巨大的 prompt,把工具说明、历史对话、任务目标全塞进去,结果模型在第 5 步之后开始胡编工具名。问题不在模型,在于编排层缺位。白皮书里强调的“循环 + 状态”其实就是编排层的最小形态:每一步把当前状态喂给模型,模型决定调哪个工具,工具返回结果写回状态,再进入下一步,直到满足终止条件。

理解这四层之后,选型就清晰了。模型层可以换,工具层按业务写,记忆层先用内存字典顶着,编排层自己写一个 while 循环就够。不要一上来就上重型框架,框架会掩盖你对状态流转的理解,等出问题的时候你连日志都看不懂。

2.2 为什么先写“单 Agent + 工具调用”而不是多 Agent 协作

白皮书里通常会提到多 Agent 协作,比如一个规划者、一个执行者、一个审查者。这个结构很吸引人,但对刚上手的人是个陷阱。多 Agent 的第一版几乎都会退化成“三个模型互相甩锅”,因为角色边界靠 prompt 约束,而 prompt 约束在多轮之后会衰减。

更稳的路径是先写单 Agent 加工具调用,把状态机跑顺。单 Agent 的循环只有三个动作:思考、调工具、观察结果。你能清楚看到每一步的输入输出,出问题能定位到具体哪一步。等单 Agent 稳定了,再把其中某些步骤抽成独立角色,这时候你已经有日志和状态快照,多 Agent 的调试成本会低很多。

我一般会建议:第一个版本不要超过 3 个工具,不要超过 8 步循环,不要引入向量数据库。先用内存和打印日志把流程跑通,确认模型能稳定选择正确的工具,再逐步加复杂度。这个顺序能帮你省下大量“玄学调 prompt”的时间。

2.3 一个最小可运行骨架需要哪些文件

按常见做法,一个能跑的最小工程包含四个部分:入口脚本、工具定义、编排循环、配置。入口脚本负责读参数和启动;工具定义把每个能力写成函数并附上描述;编排循环负责调模型、解析工具调用、执行、写回状态;配置放模型地址、密钥、超时、最大步数。

下面这个目录结构是我常用的,不依赖特定框架:

agent-demo/ ├── main.py # 入口,读取任务并启动循环 ├── tools.py # 工具注册与实现 ├── loop.py # 编排循环与状态管理 └── config.py # 模型地址、超时、最大步数

文件少的好处是,你读代码不用跳来跳去。等业务复杂了再拆包,不要在第一天就设计一个十层目录的“企业级架构”。白皮书给的是思想,落地时文件越少越容易验证。

3. 动手复现:用 Python 跑通一个可运行 Agent 骨架

3.1 环境准备与依赖安装

先确认 Python 版本,建议 3.10 以上,因为要用到类型标注和match语法。依赖只装两个:一个 HTTP 客户端和一个环境变量管理库。不要装一堆用不上的框架,装得越多,出问题时越难判断是谁的锅。

# 建议在虚拟环境里操作,避免污染全局 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate # 只装必要的两个依赖 pip install httpx python-dotenv

httpx用来发模型请求,支持同步和异步,比requests更适合后面扩展并发。python-dotenv用来从.env读配置,避免密钥硬编码在代码里。装完之后用pip list确认版本,如果公司内网有镜像源,记得先配好,否则会卡在下载。

配置方面,在项目根目录建一个.env文件,写入模型服务地址和密钥。注意不要把.env提交到版本库,这是最常见的翻车点之一。

# .env 示例,字段名按你实际使用的服务调整 MODEL_BASE_URL=http://127.0.0.1:8000/v1 MODEL_API_KEY=your-key-here MODEL_NAME=local-model MAX_STEPS=8 REQUEST_TIMEOUT=30

MAX_STEPS是循环上限,防止模型陷入死循环。REQUEST_TIMEOUT是单次请求超时,设太短会误杀慢响应,设太长会让失败任务卡住。我一般从 30 秒起步,根据实际延迟调整。

3.2 工具注册:把函数变成模型能选的“菜单”

工具层的核心是让模型知道“有哪些能力可用”。常见做法是每个工具写成一个函数,再配一份 JSON Schema 描述参数。模型根据描述决定调哪个、传什么参数。描述写得越清楚,模型选错的概率越低。

# tools.py import json from typing import Any, Callable # 工具注册表:名字 -> (函数, 描述, 参数schema) _REGISTRY: dict[str, dict[str, Any]] = {} def register(name: str, description: str, parameters: dict): """装饰器:把一个函数注册成模型可调用的工具""" def wrapper(fn: Callable): _REGISTRY[name] = { "fn": fn, "description": description, "parameters": parameters, } return fn return wrapper @register( name="calc", description="执行四则运算,输入两个数字和运算符,返回计算结果", parameters={ "type": "object", "properties": { "a": {"type": "number", "description": "第一个操作数"}, "b": {"type": "number", "description": "第二个操作数"}, "op": {"type": "string", "enum": ["+", "-", "*", "/"]}, }, "required": ["a", "b", "op"], }, ) def calc(a: float, b: float, op: str) -> str: if op == "+": return str(a + b) if op == "-": return str(a - b) if op == "*": return str(a * b) if op == "/": if b == 0: return "错误:除数不能为0" return str(a / b) return "错误:不支持的运算符" def get_tool_schemas() -> list[dict]: """导出给模型看的工具清单""" return [ { "type": "function", "function": { "name": name, "description": meta["description"], "parameters": meta["parameters"], }, } for name, meta in _REGISTRY.items() ] def call_tool(name: str, arguments: dict) -> str: """按名字执行工具,异常统一转成字符串返回给模型""" if name not in _REGISTRY: return f"错误:工具 {name} 不存在" try: return _REGISTRY[name]["fn"](**arguments) except Exception as exc: return f"错误:{exc}"

这段代码的关键点在call_tool的异常处理。工具执行失败时不要把异常直接抛给上层,而是转成字符串返回给模型,让模型有机会根据错误信息调整参数重试。这是 Agent 比普通函数调用更“抗造”的地方。参数 schema 里的enum能显著降低模型传错运算符的概率,别省。

3.3 编排循环:状态怎么在步骤之间传递

编排循环是整个骨架的心脏。它维护一个消息列表作为状态,每轮把消息和工具清单发给模型,模型返回要么是最终答案,要么是工具调用请求。如果是工具调用,就执行并把结果追加到消息列表,进入下一轮。

# loop.py import json import httpx from config import MODEL_BASE_URL, MODEL_API_KEY, MODEL_NAME, MAX_STEPS, REQUEST_TIMEOUT from tools import get_tool_schemas, call_tool def run_agent(task: str) -> str: # 状态:消息列表,第一条是系统提示,第二条是用户任务 messages = [ {"role": "system", "content": "你是一个会使用工具的助手,需要时调用工具,得到结果后给出最终答案。"}, {"role": "user", "content": task}, ] tools = get_tool_schemas() for step in range(MAX_STEPS): payload = { "model": MODEL_NAME, "messages": messages, "tools": tools, "tool_choice": "auto", } headers = {"Authorization": f"Bearer {MODEL_API_KEY}"} resp = httpx.post( f"{MODEL_BASE_URL}/chat/completions", json=payload, headers=headers, timeout=REQUEST_TIMEOUT, ) resp.raise_for_status() msg = resp.json()["choices"][0]["message"] messages.append(msg) # 没有工具调用,说明模型给出了最终答案 tool_calls = msg.get("tool_calls") if not tool_calls: return msg.get("content", "") # 逐个执行工具调用,把结果写回状态 for call in tool_calls: fn_name = call["function"]["name"] try: args = json.loads(call["function"]["arguments"]) except json.JSONDecodeError: args = {} result = call_tool(fn_name, args) messages.append({ "role": "tool", "tool_call_id": call["id"], "content": result, }) return "达到最大步数仍未完成,请检查任务或调大 MAX_STEPS"

逻辑上有三个细节值得说。第一,messages.append(msg)把模型的原始回复(含 tool_calls)存进状态,这是后续tool_call_id能对上的前提,漏了会报错。第二,工具结果必须以role: tool追加,并且带上对应的tool_call_id,否则模型不知道这个结果属于哪次调用。第三,循环上限必须有,我见过太多因为模型反复调同一个工具而烧掉额度的案例。

参数方面,tool_choice设为auto让模型自己决定是否调工具;如果你希望强制它先调某个工具,可以设成指定函数,但一般不建议,会限制模型的判断。MAX_STEPS从 8 起步,任务复杂再往上加,但每加一步都要想清楚失败回退怎么做。

3.4 入口脚本与一次完整运行

入口脚本负责把任务传进去并打印结果。为了看清每一步,我会在循环里加日志,把每轮的模型输出和工具结果打出来。调试阶段日志比什么都重要,别嫌它丑。

# main.py import logging from loop import run_agent logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") if __name__ == "__main__": task = "请计算 (12 + 8) * 3 的结果,并说明每一步用了什么工具" logging.info("任务开始: %s", task) answer = run_agent(task) logging.info("最终答案: %s", answer) print(answer)

运行python main.py,如果模型服务正常,你会看到它先调calc算12+8,再调一次算20*3,最后给出文字说明。这个过程就是白皮书里描述的“思考—行动—观察”循环的最小形态。跑通之后,把calc换成你自己的业务函数,比如查订单、读配置、调内部接口,骨架不用动。

提示:如果模型返回的工具名不在注册表里,call_tool会返回错误字符串,模型通常会据此改用正确工具。如果它连续两轮都调错,检查工具描述是否和任务语义差太远。

4. 避坑与排查:Agent 跑不起来时先看这几处

4.1 模型不调工具,只输出一段文字

现象是循环第一轮就返回了最终答案,但答案明显是编的,没有真正执行计算或查询。原因通常是系统提示太弱,或者工具描述和任务不匹配。模型在没有明确“需要时调工具”的指令下,倾向于直接生成看起来合理的文本。

解决办法是在系统提示里明确写“涉及计算或数据查询时必须调用工具,不要凭记忆回答”,并把工具描述写具体。如果还不行,检查tools字段是否真的传进了请求体,有些服务对字段名大小写敏感。我一般会先用一个极简任务测试,比如“计算 1+1”,确认工具调用链路通了再上复杂任务。

4.2 工具调用参数解析失败

现象是json.loads抛异常,或者参数缺字段导致工具函数报错。原因是模型返回的arguments偶尔不是合法 JSON,尤其在参数里有中文或特殊符号时。上面的代码用 try 兜底成空字典,但空字典会让工具函数因为缺参数而失败。

更稳的做法是在工具函数里对必填参数做校验,缺参数时返回明确的错误提示,让模型知道缺了什么。另外,参数 schema 里把required写全,能减少模型漏传的概率。如果某个参数是枚举,一定用enum约束,别指望模型自己收敛。

4.3 循环停不下来,反复调同一个工具

现象是日志里同一个工具被调了五六次,参数几乎一样。原因通常是工具返回的结果模型“看不懂”,或者结果里包含错误信息但模型没意识到要换策略。比如工具返回“错误:除数不能为0”,模型可能反复重试同样的参数。

解决办法有两个:一是在工具返回里加上明确的下一步建议,比如“请更换除数后重试”;二是在编排层加一个重复调用检测,如果连续两次调同一个工具且参数相同,就强制终止并返回当前状态。后者是后悔药,能防止额度被烧光。

4.4 长任务上下文超限

现象是任务跑到第七八步时请求报错,提示上下文长度超限。原因是消息列表只增不减,历史工具结果越堆越多。解决办法是在编排层做裁剪:保留系统提示、最近若干轮消息和所有工具调用的摘要,把早期的详细结果压缩成一句话。

常见做法是给消息列表设一个 token 预算,超过就从头部的工具结果开始摘要。不要直接删消息,删了会导致tool_call_id对不上。摘要时保留工具名和关键结论即可,细节可以丢。

4.5 本地模型和远程模型行为不一致

现象是同一份代码,接本地模型时工具调用正常,接远程模型时格式对不上。原因是不同服务对工具调用字段的实现有差异,有的用tool_calls,有的用function_call,有的要求tools和functions二选一。

解决办法是抽象一层适配器,把不同服务的返回统一成内部格式。上面代码里只处理了tool_calls这一种,如果你要接多个服务,在run_agent里加一个normalize_response函数做转换。别在业务代码里到处写 if-else 判断服务类型,那是维护噩梦。

5. 进阶技巧:把骨架变成能长期用的 Agent

跑通最小骨架只是起点。真正决定一个 Agent 能不能长期用的,是两件事:工具描述的质量和状态的可观测性。工具描述不是写给人看的文档,是写给模型看的接口契约,措辞要精确到“什么情况下用、什么情况下不用”。我一般会在描述里加一句反例,比如“本工具只做四则运算,不处理单位换算”,能明显减少误调用。

状态可观测性方面,建议把每轮的消息列表快照落盘,按任务 ID 存成 JSON。出问题时不用重跑,直接看快照就能定位是哪一步的输入导致了错误输出。这个习惯帮我省下过大量复现时间,尤其是那种“偶发失败”的任务。

再往上走,可以引入一个轻量的验证步骤:在模型给出最终答案后,用一个独立的提示让它自查“答案是否基于工具返回的结果”。这一步不需要额外模型,复用同一个服务即可,但能拦住相当一部分幻觉。代价是多一次请求,值不值得看你任务的容错要求。

最后说一个我踩过的坑:不要过早引入向量数据库和复杂记忆。我早期一个项目上来就接了向量检索,结果调试时根本分不清是检索召回错了还是编排逻辑错了,排查了两天才发现是工具描述里少写了一个参数。后来我把记忆层砍掉,先用内存字典跑通全流程,问题当天就定位了。骨架越简单,出问题时你越有把握。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询