资讯动态

Agent从能聊天到能干活:白皮书核心架构与最小实现

发布时间:2026/10/6 9:56:56 来源:尧图企业网站定制
简介Google在2024年首版《Agents》后发布进阶白皮书《Agents Companion》聚焦从概念普及转向工程化落地。该白皮书配套的可运行源码包面向希望掌握代理技术架构设计与企业级实践的开发者。压缩包共3个文件包含HTML入口页面、inscode在线运行配置及gitignore版本控制文件整体仅9KB体量精简便于快速启动和二次修改。已有135人学习下载适合AI应用研发者、技术决策者及技术爱好者用于理解代理技术的实际运转方式。通过运行源码可直观观察白皮书阐述的agent调用逻辑、工具集成方式并对照文档梳理从原型Demo到生产部署的关键环节虽然包体小巧但配合白皮书理论能形成完整的认知闭环为后续自建Agent系统提供基础脚手架。1. 白皮书发布Agent 从“能聊天”到“能干活”的分水岭Google Agent 白皮书的发布把业内已经吵了一年的问题推到了台面上Agent 到底是不是大模型的套壳如果你做过真实项目就会知道套壳和 Agent 之间差着十万八千里。我见过太多团队拿着一个大模型 API 就宣称在做 Agent结果连“让它调用一个计算器工具再返回结果”都要调三天。白皮书这次把 Agent 的架构、推理循环、工具调用规范、上下文管理拆开摆在了明面上而且罕见地配套了可运行源码这让“照着设计图落地”第一次有了标准答案。这篇笔记我就按一线踩坑的视角把白皮书的核心设计翻译成你能直接跑起来的最小框架再说清楚参数怎么设、哪些地方会让你半夜翻车。适合正在做 Agent 项目、或者准备从零搭一个稳定 Agent 的工程师。2. Agent 白皮书的核心三个组件和一个循环2.1 推理循环ReAct 范式为什么成了默认选择白皮书把 Agent 的工作方式收敛成了一个循环模型产出一个动作系统执行这个动作把观察结果喂回模型再产出下一个动作。这就是 ReAct 范式的工程化表述。你去看白皮书配套的可运行源码主循环基本就是这样一个结构没有多余的花活。这个循环看起来简单但它是 Agent 区别于普通聊天应用的标志。聊天模型的每次响应都是终点而 Agent 的每次响应都是中间态。我见过不少人一上来就上复杂的规划器或多智能体框架结果第一个版本就死在了调试复杂度上。白皮书的做法反而务实先用一个单智能体的 ReAct 循环跑通再考虑加规划层。代码结构上这个循环只需要三个部分一个把用户请求转成系统消息的入口一个接收模型输出并解析动作的解析器一个执行工具并把结果追加回上下文的执行器。def run_agent(task: str, max_steps: int 8) - str: messages [{role: system, content: SYSTEM_PROMPT}, {role: user, content: task}] for step in range(max_steps): response llm_chat(messages) action parse_action(response) if action[name] finish: return action[args][answer] observation execute_tool(action[name], action[args]) messages.append({role: assistant, content: response}) messages.append({role: tool, content: observation}) return 超过最大步数任务未完成llm_chat是模型调用占位函数你可以对接 OpenAI 兼容接口也可以接本地模型。parse_action负责把模型的输出解析成结构化动作这是最关键的一步下文会专门讲。execute_tool查工具注册表执行对应函数。循环里必须有一个max_steps兜底这是防止模型反复横跳导致死循环的第一道保险。2.2 工具层设计为什么白皮书强制 JSON 输入输出白皮书里有一个容易被忽略但极其重要的规定所有工具必须接收 JSON 参数、返回 JSON 结果。这个约束不是拍脑袋定的它直接决定了 Agent 的稳定性。如果工具返回自由文本模型就无法可靠地从中提取关键字段如果工具接收自由文本模型生成工具调用时就会在参数格式上频繁出错。我一开始做 Agent 时工具返回的是人类可读字符串比如“天气晴温度25℃”结果模型在下一轮推理时经常提取出“晴”和“25”作为两个孤立字段导致后续工具调用参数错乱。改成 JSON 后模型直接从{condition: sunny, temperature: 25}里取字段错误率立刻降了一个量级。白皮书源码里的工具注册器也体现了这个思路每个工具都有 name、description、parameters 三个字段执行结果统一包一层{status: success, data: ...}。from dataclasses import dataclass, field import json dataclass class ToolSpec: name: str description: str parameters: dict REGISTERED_TOOLS {} def register_tool(name: str, description: str, parameters: dict): def wrapper(func): REGISTERED_TOOLS[name] { function: func, spec: ToolSpec(name, description, parameters) } return func return wrapper register_tool( namecalculator, description计算四则运算表达式表达式必须是合法的数学公式, parameters{ type: object, properties: {expression: {type: string}}, required: [expression] } ) def calculator(expression: str) - str: try: result eval(expression) return json.dumps({status: success, data: {result: result}}) except Exception as e: return json.dumps({status: error, message: str(e)})这段代码的关键在register_tool装饰器它把函数的__name__映射到工具名同时把白皮书强调的 JSON Schema 绑定在工具上。description字段不是给人看的注释它是喂给模型做工具选择的上下文。写描述时要站在模型的角度写清楚“这个工具在什么场景下用、什么输入才能得到正确结果”。比如calculator的描述里写“表达式必须是合法的数学公式”模型就会尽量避免传一个残缺表达式过来。2.3 动作解析模型输出不是代码是待解析的数据白皮书配套源码里有一个让很多人初次上手时疑惑的设计模型输出不直接执行而是先经过一个解析器再决定下一步。这是因为模型返回的自然语言里可能混着思考过程、工具调用和最终答案如果不加约束直接执行会出现灾难性的后果——比如模型在思考里写了一句“我应该调用删除接口”解析器直接当成工具指令执行了。白皮书源码的常见做法是约定模型输出一个固定 JSON 结构{thought: ..., action: {name: ..., args: {...}}, observation: ...}。你应该用 system prompt 把这条约定钉死同时把工具清单和调用样例一次性塞进去。解析层要做两层防护第一层用 JSON 解析库尝试解析第二层在解析失败时用正则提取action.name和action.args作为兜底不要因为一次格式错误就让整个 Agent 崩溃。def parse_action(model_output: str) - dict: try: data json.loads(model_output) if action in data: return data[action] except json.JSONDecodeError: pass match re.search(rname\s*:\s*(\w), model_output) if match: tool_name match.group(1) args_match re.search(rargs\s*:\s*({[^}]}), model_output) args json.loads(args_match.group(1)) if args_match else {} return {name: tool_name, args: args} return {name: finish, args: {answer: model_output}}这段兜底逻辑是血泪经验换来的。小模型和部分量化模型在输出 JSON 时经常漏掉一个右括号或者多一个逗号如果你在这一层直接抛异常整个循环就断了。兜底正则虽然丑但它能把大多数格式错误拉回正轨。注意正则的容量问题工具多、参数嵌套深的时候这个兜底会变得不可靠所以它只是保护网主力还是要靠格式化输出约束。3. 把白皮书翻译成可运行源码从设计到最小闭环3.1 构建最小可运行框架白皮书不是看懂的是跑通的白皮书配套的可运行源码在我看来最大的价值不是告诉你 Agent “应该”是什么样而是给你递了一个“能跑起来的最小骨架”。拿到手之后不要急着往上加功能先把这个骨架在一个简单任务上跑通确认推理循环能完整转一圈用户提问、模型思考、调用工具、观察结果、给出答案。我一般会先用“天气查询 计算器”这类两个以内的工具做冒烟测试。为什么是两个因为一个工具太简单无法暴露工具选择模型的误判问题两个工具则能看出模型能不能根据用户问题在工具之间做正确路由。你可以在REGISTERED_TOOLS里注册两个工具然后跑一个复合任务“计算 15 的平方再告诉我 30 加这个结果等于多少”。如果 Agent 能先调计算器算 15 的平方再把结果拿来算加法说明循环是通的。def build_system_prompt(tools: list) - str: tool_descriptions [] for tool in tools: spec tool[spec] tool_descriptions.append( f- {spec.name}: {spec.description}\n 参数: {json.dumps(spec.parameters)} ) return SYSTEM_PROMPT_TEMPLATE.format( tool_list\n.join(tool_descriptions) )build_system_prompt把工具注册表渲染成 system prompt 里的工具清单目标是让模型每一次推理都能看到全部可用工具的规格。白皮书源码里对这部分处理得很细工具说明要放在 system 消息的尾部离模型开始生成的位置越近工具选择的准确率越高。这个细节在长上下文中尤其明显我就是因为工具的 description 太长、被截断导致模型反复选择不存在的工具名浪费了整整一天排查。3.2 上下文管理白皮书没明说但源码里藏着的关键参数白皮书源码里有一个很容易被忽略的部分上下文的修剪逻辑。Agent 每转一圈工具返回的 observation 就会追加进消息列表转十圈就多了二十条消息。token 会爆炸而更隐蔽的问题是“上下文污染”——太早的工具输出会干扰模型后续的判断。我之前跑一个需要连续查三次数据库的任务模型在第三轮突然开始复盘第一轮的查询结果输出明显偏向于早期数据。常见做法是把消息列表压缩成两种形式截断和摘要。截断最简单超过max_context_tokens就从最早的消息开始丢摘要则需要另一个模型调用对早期内容做压缩。白皮书的做法偏向于结构化压缩——不压缩自然语言而是把工具调用的参数和结果直接替换成一条摘要记录。我这里给一个底层思路把观察结果长文本放进一个临时存储消息列表里只留一个引用 ID。def append_observation(messages: list, observation: str, max_obs_chars: int 500) - str: if len(observation) max_obs_chars: observation_id fobs_{len(messages)} observation_store[observation_id] observation short_ref f[长观察结果已存入 {observation_id}不再展开] messages.append({role: tool, content: short_ref}) return observation_id messages.append({role: tool, content: observation}) return observation这段代码引入了一个observation_store字典做外部存储消息列表只保留摘要。这样做有两个好处上下文窗口不被长文本吃光模型在后续推理时不会被密集数据干扰。代价是模型如果需要查看完整结果你得显式提供一个“读取观察详情”的工具。这个取舍白皮书没有展开讲但实现时一定要考虑尤其是你的工具返回日志、数据库查询结果这种动辄几千字符的对象时不做压缩你的 Agent 跑不到第五轮就会报上下文溢出。3.3 模型选型与参数白皮书源码默认值的取舍白皮书配套源码在模型调用层留了参数接口但默认值的选择很有意思temperature0、max_tokens1024。前者保证工具选择的行为稳定可复现后者限制单次输出长度避免模型生成一篇小作文而不是一个 JSON 动作。这个组合我在实际项目中沿用至今只在个别需要创意生成的 Agent 里才把 temperature 调高到 0.3。还有一个参数白皮书源码里出现过但容易被忽略stop参数。很多模型支持在生成到指定字符串时停止你可以把stop[\n\n]或[Observation:]用来让模型在思考部分写完就停下来而不是一路写到工具调用结束。这样解析器拿到的文本更干净正则兜底的命中率更高。我在接一些开源模型时发现它们的 JSON 输出经常不闭合后来在生成时强制加了两条换行作为结束符解析成功率大幅提升。LLM_CONFIG { temperature: 0, max_tokens: 1024, top_p: 0.9, }top_p0.9是我后来加上去的配合temperature0使用。严格说temperature0时top_p已经不起作用但不同的 API 实现对这个组合的处理有差异保留它可以避免部分实现里的默认top_p1在解码时引入额外的随机性。这个参数表你可以在不同模型之间对比测试同一个任务跑十次看工具选择是否完全一致不一致就说明采样参数没有压住乱蹦。4. Agent 运行避坑5 个必踩的坑与对应解法4.1 模型虚构工具名幻觉不是模型的错是你的清单没喂好现象Agent 在第三轮突然调用一个search_web工具但你注册列表里根本没有这个工具循环直接抛 KeyError。原因模型在长上下文中忘记了可用工具的完整清单尤其是工具描述占比显著、而早期工具名已经被上下文挤压的情况下模型会“创造”一个看起来合理的工具名。解决每次调用模型前重新生成 system prompt把工具清单放在最靠近用户消息的位置同时给模型一个显式的“可用工具”提示词列出工具名和一句话描述。另外在execute_tool里加一个软兜底找不到工具时返回一个错误提示给模型让它重新选择而不是直接让进程崩溃。我发现用中文提示“未找到该工具请从可用工具清单中选择”时模型下一轮通常能修正过来。4.2 JSON 输出格式不稳定小模型的通病靠双重解析兜底现象模型偶尔输出{thought: ..., action: {name: calculator, args: {expression: 15*15}}漏了右花括号json.loads直接抛异常。原因开源小模型和部分量化模型对 JSON 语法的遵循能力不稳定尤其在展开思考链时占了太多 token最后草草结束导致括号不闭合。解决解析层永远不裸调json.loads。先尝试标准解析失败后用括号计数法补全缺失的右括号再失败就退回正则提取。这套三层方案我已经用了半年模型偶尔吐出的脏 JSON 都能被拉回正轨。注意包一层重试逻辑解析失败可以让模型重新生成一次但重试次数不要超过 2 次否则成本不可控。4.3 死循环Agent 反复调用同一个错误工具现象Agent 反复调用计算器算同一个表达式每次都得到一样的错误结果但模型就是不停止直到max_steps触底。原因模型错误地认为“再做一次就能成功”或者工具返回的错误信息不够明确模型无法判断失败原因。解决max_steps不能只做最终保险要加“重复动作检测”。维护一个最近 3 步的动作哈希表如果连续重复 3 次以上中断循环并返回“检测到重复动作任务终止”。同时优化工具的报错信息错误不要只返回{status: error}要把原因写进去比如“表达式语法错误括号不匹配”模型下一轮就会调整策略而不是重试原样。4.4 长上下文跑崩token 超限前的 3 个预警信号现象任务进行到第十轮左右API 突然返回 400 错误提示maximum context length exceeded。原因上下文没有做压缩工具输出和消息对账全部累积在消息列表里轻松突破模型窗口上限。解决抓住三个信号提前处理。信号一消息列表长度超过窗口的 70%信号二单个工具返回超过 2000 字符信号三模型开始重复提及早期步骤的内容。这三个信号出现任意一个立即上摘要策略把旧消息压缩成一条概述或者把外部存储的引用替换进列表。具体实现参考前面append_observation里的方案。4.5 工具返回值被模型忽略observation 要显眼现象工具返回了计算结果但模型在下一轮完全不引用这个结果直接自己算了一个答案。原因工具返回内容在消息里是和 assistant 消息平级的部分微调模型不擅长区分 tool / observation 角色的消息会把工具结果误认为是历史对话。解决把工具返回内容包装成显眼的格式比如在内容前加[工具返回开始]和[工具返回结束]标记。这个做法在严格按 OpenAI 角色规范训练的模型上效果一般但对国产开源模型和部分自托管模型效果显著。我还试过把工具结果复制一份放到用户消息里用【工具结果】前缀标注命中率更高但会吃掉双倍 token适合工具数量少的场景用。5. 验证方法用评测集把 Agent 行为钉死在预期轨道上白皮书源码里有一个我不确定是否官方强调过、但实际项目中必须补上的部分评测集。跑通十个手写用例只能说明 Agent“能用”说明不了“稳定”。我会给每个 Agent 项目建一个eval_cases.json里面放 20 到 50 条任务每条任务标注三样东西输入、期望调用的工具序列、最终答案应包含的关键词。然后写一个脚本批量跑统计工具选择准确率和任务完成率。def evaluate_agent(eval_cases: list, run_fn) - dict: tool_accuracy [] completion_rate [] for case in eval_cases: result run_fn(case[input]) expected_tool case[expected_tool_sequence] actual_tool result[tool_trace] tool_accuracy.append(actual_tool expected_tool) completion_rate.append(result[answer_contains_keywords]) return { tool_accuracy: sum(tool_accuracy) / len(tool_accuracy), completion_rate: sum(completion_rate) / len(completion_rate) }这个脚本跑完之后你会发现很多手测时觉得“不错”的 Agent在评测集上工具准确率只有六成。问题通常是工具描述写得不够精准模型经常选错工具说明 description 里有歧义。你会被迫回去重写每条工具的描述这个过程很磨人但评测集就是用来干这个的。我最后养成的习惯是每次改动 prompt 或新增工具后先跑一遍评测集再看效果而不是凭一次对话表现下结论。评测集也要维护。你的 Agent 每次遇到线上新问题如果发现是模型行为异常就把这个案例加进评测集。这样三个月之后你可以明确说出你的 Agent 对哪类问题驾驭得很好、对哪类问题仍然容易出错。比任何人拍胸脯的保证都可靠。这也是我看完白皮书源码后最想让你带走的东西Agent 不是写完就结束的代码它是需要持续用数据校准行为的系统。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑