Agent 形态变化快,这不是新鲜事。从早期的单轮工具调用,到 ReAct 循环、Plan-and-Execute、多 Agent 协作,再到 Harness、Skill、MCP 这些新词不断冒出来,真正做工程的团队会发现一个尴尬事实:你今天为某个 Agent 框架写的调度逻辑、工具封装、状态管理,三个月后大概率要重写。问题不在 Agent 本身,而在于我们把基建建在了“形态”上,而不是建在“结构”上。
这篇文章想聊清楚一件事:Agent Infra 到底该为谁而建?我的观点是,Infra 不应该跟着具体 Agent 产品走,而应该服务 Agent 运行时的共性层,也就是所有 Agent 都需要的执行循环、记忆、工具路由、控制、可观测这五层。下面会拆开讲每一层的抽象边界,哪些东西值得建,哪些东西现在碰就是过度设计,以及一套可以分批落地的建设路径。
如果你正在做 Agent 应用,或者团队正在选型底层框架,这篇文章可以帮你少走弯路。
1. 先看清楚问题:Agent 形态变化到底变的是什么
先不急着谈 Infra,先梳理一个事实:过去一年 Agent 的形态到底变了多少次。
最早大家做的是“大模型 + 工具调用”,本质上一个函数里塞几个 Tool,让模型选一个执行,一次性返回结果。后来发现单次调用解决不了复杂任务,于是有了 ReAct 风格的循环:推理、行动、观察、再推理。再后来是 Plan-and-Execute,先让模型生成一个计划,再逐步执行。到了多 Agent 阶段,又开始拆角色,有 Planner、Executor、Reviewer,每个角色一个模型实例或一个 Prompt 配置。
最近热词里的 Harness、Skill、CLI 通用标准,本质上又是新一轮抽象,试图把 Agent 的“外壳”和“技能”规范化。
这些变化看起来眼花缭乱,但拆开看,所有 Agent 形态都共享同一个最小结构:
- 感知:接收用户输入、工具返回结果、环境状态。
- 决策:由模型根据当前上下文决定下一步动作。
- 行动:调用工具、写代码、发请求、操作外部系统。
这三个动作周而复始,构成了 Agent 的基本循环。形态变化的只是“外壳”,循环基本没变。
所以,Agent Infra 最该抓住的是那个“循环”,而不是循环外面不断变化的“戏法”。
如果你把基建建在具体形态上,比如专门为某个多 Agent 框架写一套消息协议,为某个 Harness 写一套 Skill 加载器,那形态一变,基建就废。反过来,如果你把基建建在“循环 + 上下文 + 工具 + 控制”上,形态怎么变,底层都能接住。
2. Agent Infra 的边界:该为谁而建
讨论 Infra,最难的是划边界。边界划得太窄,框架一换就推倒重来;边界划得太宽,什么都抽象,结果什么都不能直接用。
这里给一个 Agent 系统的四层划分,方便讨论:
| 层级 | 职责 | 典型例子 | 变化速度 |
|---|---|---|---|
| 产品层 | 面向用户的界面、交互流程、Agent 人设 | 客服 Agent、编程助手、数据分析 Agent | 极快 |
| 运行时层 | 循环执行、上下文管理、工具调度、记忆读写 | Agent 执行引擎、Memory Store、Tool Registry | 中慢 |
| 模型层 | 推理能力、模型路由、Prompt 模板 | GPT、Claude、Qwen,模型网关 | 中 |
| 基础资源层 | 算力、对象存储、数据库、消息队列 | GPU 集群、向量库、Redis | 慢 |
Agent Infra 应该服务的是运行时层,外加横跨所有层的控制与可观测能力。
原因很简单:模型层变化快但你控制不了太多,基础资源层稳定但和 Agent 没有直接关系,产品层变化快到你根本追不上。只有运行时层,是 Agent 系统的“骨架”,它的变化速度相对可控,又足够通用。
判断你建的基建到底对不对,有一个很简单的标准:如果今天把你现在用的 Agent 框架换成另一个,你的记忆模块、工具注册表、日志链路、权限控制还能不能原样保留?
如果能,说明你建的是 Agent Infra。如果不能,说明你建的只是某个框架的插件,本质上是“项目代码”,不是“基础设施”。
另一个反向判断标准是:你建的东西,是不是在替产品层做决定?比如你规定 Agent 必须用某个 Prompt 模板、必须按照某个角色顺序执行,那你就越界了。运行时可以提供服务,但不应该规定产品怎么设计。
换句话说,Agent Infra 是“可复用的执行环境”,而不是“另一个 Agent 框架”。
3. 运行时:Agent 的循环是基础设施的核心
Agent Infra 最核心的部分,是那个“执行循环”。不管形态怎么变,Agent 本质上就是一个循环:模型推理,决定动作,执行动作,把结果放回上下文,再进入下一轮。
这一层该用什么方式实现?两种主流方案。
第一种是“让每个 Agent 自己写循环”。也就是你用一个通用大模型 SDK,然后在业务代码里自己写while循环,自己维护上下文,自己调工具。这种方式灵活,但坏处是循环逻辑散落在各个业务代码里,工具调用失败要自己处理,上下文管理要自己实现,出了问题排查也很费劲。
第二种是“运行时统一处理循环”。也就是把循环抽象到平台层,业务只需要提供工具和终止条件,运行时负责调度。这种方式更适合做成 Infra,因为循环的可靠性、重试、日志、成本控制都能统一处理。
下面给一个最简的 Agent 运行时循环伪代码,展示运行时层的核心逻辑:
class AgentRuntime: def __init__(self, model_client, tool_registry, max_steps=20): self.model_client = model_client self.tool_registry = tool_registry self.max_steps = max_steps self.history = [] def run(self, task: str) -> str: self.history.append({"role": "user", "content": task}) for step in range(self.max_steps): response = self.model_client.chat(self.history) action = self.parse_action(response) if action.type == "finish": return action.output if action.type == "tool_call": tool_result = self.tool_registry.call( action.tool_name, action.arguments ) self.history.append({ "role": "tool", "tool_name": action.tool_name, "content": tool_result }) else: self.history.append({"role": "assistant", "content": response.text}) raise MaxStepsExceededError(f"Reached max steps: {self.max_steps}")从这个伪代码里能看出,运行时要做的事不多但很关键:维护循环、调用模型、批量工具执行、结果回填、步骤上限控制。
这里就要提到最近热词里经常看到的 Harness 和 Agent 的区别。Harness 可以理解为一个更完整的“执行容器”,不仅管理模型推理循环,还管理上下文打包、安全性检查、任务边界,让 Agent 开发者只关注“技能”和“工具”。普通 Agent 框架则更偏向让开发者直接写业务逻辑。
其实不用纠结 Harness 和 Agent 谁高级。对 Infra 而言,你要的是“执行容器”这件事稳定存在。至于它叫 Harness 还是 Runtime,是以后的事。
运行时设计最值得注意的还有两个点。
第一,停止条件不能只靠模型自觉。很多 Agent 卡死,就是因为模型一直不输出 Finish,或者一直重复调用同一个工具。所以步骤上限、Token 上限、执行时间上限必须有,缺一个都不行。
第二,失败恢复要有独立策略。工具调用失败很常见,但失败不应该让整个 Agent 崩溃,而是应该有“重试、换工具、告知模型、终止”四种可选策略。这四种策略应该由运行时统一支持,而不是每个 Agent 自己写。
4. 记忆:最容易被业务形态污染的层
记忆是整个 Agent 系统里最容易被误解、也最容易被“业务化”的层。很多人做 Agent 记忆,是从自己的业务需求出发,设计了一张 UserID + ChatID + Message 的数据库表,然后再加一个向量库存 embedding。结果 Agent 形态一换,这套记忆结构完全不能用。
这是典型的“把记忆建在了产品层”。
从运行时角度看,Agent 记忆至少可以分成四类:
| 记忆类型 | 作用 | 典型实现 | 生命周期 |
|---|---|---|---|
| 短期对话记忆 | 当前任务的多轮上下文 | 消息列表、上下文窗口 | 任务内 |
| 工作记忆 | 当前任务中间结果、临时状态 | JSON 状态、变量池 | 任务内 |
| 长期记忆 | 跨任务的用户偏好、历史事实 | 向量库 + 元数据库 | 跨任务 |
| 程序化记忆 | 工具使用习惯、任务模板 | Skill、少量示例、规则 | 长期稳定 |
Infra 层要做的,是提供一套统一的记忆读写接口,至于底层存哪里、用什么向量库,那是实现细节。
最简单的接口设计:
class MemoryStore: def save_episode(self, agent_id: str, task_id: str, messages: list) -> None: ... def load_episode(self, agent_id: str, task_id: str) -> list: ... def save_fact(self, agent_id: str, key: str, value: str, namespace: str) -> None: ... def query_facts(self, agent_id: str, query: str, namespace: str, top_k: int) -> list: ... def save_state(self, agent_id: str, task_id: str, state: dict) -> None: ... def load_state(self, agent_id: str, task_id: str) -> dict: ...这套接口的价值在于:不论你今天是单 Agent,明天是多 Agent,后天是 Harness,你的记忆模块都不需要重写。
更重要的一个点:任务状态的管理最好采用快照或事件追加的方式,而不是直接改数据库记录。Agent 在多轮执行中,状态随时可能变化,而且一旦失败要能恢复到某个检查点。用事件日志记录每一步状态变化,比粗暴覆盖数据库更容易回溯和排查。
这里也顺带推荐一个思路:每个任务开始时,给任务分配一个task_id,并把初始请求、初始上下文、模型配置、工具列表全部记录为一条“任务快照”。任务执行完后,把最终输出、Token 消耗、工具调用序列也记录到同一份快照。这样长期记忆才能有数据可用,否则长期记忆存进去的都是“无来源的碎片”。
5. 工具:Skill、MCP 与工具路由的取舍
工具层是 Agent 产品差异最大的地方,也是 Infra 设计最容易过度抽象的地方。
现在业界有几个容易混淆的概念:Tool、Skill、MCP。
简单理解:
- Tool 是单个可调用能力,比如“搜索天气”“读取文件”。
- Skill 是一组有组织的技能,通常包含工具、提示词、校验逻辑和示例,可以理解为一个“能力包”。
- MCP 是一种让模型和外部数据/工具连接的开放协议,核心价值是把工具从“写死代码”变成“动态发现和调用”。
对 Infra 团队来说,工具层真正值得建的东西只有三件:工具注册表、工具调用审计、工具结果标准化。至于工具本身,能直接对接 MCP 就对接 MCP,不要自己发明一套协议再走一遍适配。
工具注册表的设计要点,是把工具的元数据和实现解耦。注册表里存的是 schema,实际调用时通过名称找到 handler。
TOOL_REGISTRY = {} def register_tool(name: str, schema: dict, handler=None): TOOL_REGISTRY[name] = { "schema": schema, "handler": handler, } def call_tool(name: str, arguments: dict): if name not in TOOL_REGISTRY: return {"error": f"tool {name} not found"} entry = TOOL_REGISTRY[name] # 这里统一做参数校验、鉴权、审计 validate_arguments(entry["schema"], arguments) audit_tool_call(name, arguments) result = entry["handler"](**arguments) return {"tool": name, "result": result}工具路由不要做得太重。Agent 需要的是一个“能通过名字和参数找到执行器”的注册表,而不是一个复杂的服务发现框架。你真正要花心思的是工具调用审计:谁在什么任务里调了哪个工具、传了什么参数、返回了什么结果。有了这个记录,后面做安全审计和效果评估才有数据。
一个容易踩坑的地方是:试图把所有工具统一成一种“万能调用格式”。实际上,不同工具的参数风格差异巨大,过度抽象只会让每个工具都要写一段适配代码。合理的做法是保留工具 schema 的灵活性,只在结果回传给模型之前做统一格式化,这样既能简化适配,又能保证模型可理解。
另外要留意工具冲突问题。当多个 Agent 共享同一批工具时,要防止一个 Agent 的执行错误影响其他 Agent。Infra 层可以给工具调用加资源隔离或者并发限制,比如同一时刻某个工具最多被多少个 Agent 调用,这个限制在工具注册表上直接配置。
6. 控制平面:Agent 安全的落地形态
Agent 安全问题最近提得很多。但真正的问题是:安全控制应该放在哪一层?
答案应该是独立的“控制平面”,而不是散落在各个 Agent 代码里。控制平面和运行时平行,负责检查、放行、拦截、审计。
Agent 安全最少要覆盖五个方面:
- 身份与权限:这个 Agent 代表谁,能访问哪些资源。
- 工具级访问控制:不是所有 Agent 都能调用所有工具。
- 数据保护:出域数据、日志数据、记忆数据要脱敏。
- 操作审批:高危操作(发消息、付钱、删数据)需要人工确认。
- 依赖审计:Agent 依赖的模型、框架、第三方库要能追溯版本。
其中工具级访问控制是刚需。比如一个只读的文档总结 Agent,就不应该被允许调用“发送邮件”工具。这个约束不能写在业务代码里让开发者自觉遵守,而应该由运行时统一强制。
一个精简的访问控制示例:
AGENT_POLICIES = { "doc_summarizer": { "allowed_tools": ["read_file", "search_docs"], "allowed_domains": ["*.internal.example.com"], "max_tokens": 100000, } } def check_tool_allowed(agent_id: str, tool_name: str) -> bool: policy = AGENT_POLICIES.get(agent_id, {}) return tool_name in policy.get("allowed_tools", [])安全控制不能只做拦截,还要做记录。每一次拦截都应该有一条日志,说明“哪个 Agent 在哪个任务里,试图调用什么工具,为什么被拦截”。这套日志不仅是合规要求,更是后续调整权限策略的依据。
在多 Agent 协作场景下,控制平面还要增加一层信任边界。Agent A 调用 Agent B 时,B 应该能看到“调用者是谁、调用目标任务是什么、权限边界是什么”。不能让一个低权限 Agent 通过调用高权限 Agent 来绕过访问控制。
这里也要提醒一句:安全不是一次配置就能完成的。Agent 产品上线后要定期检查权限配置,新工具上线时要重新做一次权限评估。最好把 Agent 安全策略纳入 CI/CD 审查,而不是等出了事再补救。
7. 可观测性:没有 trace 就谈不上 Agent Infra
Agent 系统排查问题比传统后端系统难得多。传统系统一次请求的调用链是清晰的,而 Agent 的一次任务可能包含十几轮模型推理、几十次工具调用,而且每一步都可能出现意外。
所以 Agent Infra 必须从第一天就内建可观测性。三个核心维度:
- 模型调用观测:每次模型调用的 prompt、响应、Token 消耗、延迟。
- 工具调用观测:哪个 Agent 调了哪个工具、参数是什么、结果是什么、耗时多久。
- 任务级观测:整个任务从开始到结束的完整 trace、状态变化、哪一步花了最多时间。
实际埋点时要做到“自动采集”,不能指望业务开发者手动加日志。
@contextmanager def trace_step(step_name: str, **attributes): step_id = generate_id() start = time.monotonic() try: yield step_id except Exception: attributes["error"] = traceback.format_exc() raise finally: elapsed = time.monotonic() - start emit_trace({ "step_id": step_id, "name": step_name, "elapsed_ms": round(elapsed * 1000, 2), **attributes, })有了这份 trace,你就可以回答这些问题:Agent 为什么花了 3 分钟?因为模型推理慢还是工具调用慢?工具被连续调用了 5 次都没有失败,是不是 Prompt 设计有问题?Token 都消耗在哪里了?这些问题不靠 trace,靠经验猜,效率极低。
可观测性和评估是两回事。可观测性回答的是“Agent 发生了什么”,评估回答的是“Agent 做得好不好”。前者是底子,后者是上层。很多团队一上来就想做 Agent 效果评估,结果发现连 trace 都没有,效果不好没法归因。所以顺序应该是:先可观测,再评估。
8. 分批建设的路径:先最小运行,再逐步补齐
这一节给你一套 Agent Infra 的分阶段建设路径。核心原则是:不要一步到位,也不要什么都不建。
第一阶段:先跑通最小运行时。选一个 Agent 框架或自己写一个轻量循环,先把“模型调用 + 工具调用 + 上下文维护”跑通。这个阶段不要写太多抽象,把任务跑起来是最重要的。
第二阶段:固定工具注册表和审计日志。不管用任何框架,工具调用都要经过一个统一入口,并且记录完整调用日志。这一步做完,你就有了排查问题的基础。
第三阶段:抽象记忆和状态。把记忆从业务代码里抽出来,用统一接口读写。这一步做完,你换 Agent 框架时记忆模块就能保留下来。
第四阶段:接入控制平面。给 Agent 配置权限、工具白名单、数据脱敏规则。这个阶段越早做越好,但可以不必一开始就做全套,先保住“最小权限 + 高危操作审批”。
第五阶段:多 Agent 协作与路由。前四步都做完了,再开始考虑多 Agent 之间的消息路由、信任边界、任务分发。很多团队一上来就搞多 Agent,结果每个 Agent 连单 Agent 的稳定性都没解决,最后整个系统非常脆弱。
再强调一次:Agent 形态还在飞速变化,你今天为“某个形态”建的基建,很有可能明天就要推翻。但运行时的循环、工具注册表、统一记忆接口、控制平面、可观测性,这些无论形态怎么变都需要。建设的重心放在这五件事上,性价比最高。
9. 常见误区与排查清单
下面整理了一份 Agent 基建建设过程中常见的问题和排查方向,方便你对照检查。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 循环不终止 | 没有设置步骤上限,或模型一直不输出结束标记 | 查看运行时日志中步骤数 | 配置 max_steps 和 Token 上限 |
| 工具被连续重复调用 | Prompt 指令不清或工具返回结果模型不认可 | 查看 trace 里的工具调用序列 | 优化 Prompt,给结果加明确格式 |
| 上下文溢出 | 每轮都把完整历史塞给模型 | 计算每轮 Token 消耗 | 引入上下文裁剪、摘要压缩、滑动窗口 |
| 任务中断后无法恢复 | 没有状态快照,状态只存在内存里 | 检查任务状态记录 | 引入任务级快照和事件日志 |
| 记忆串话 | 不同任务共用一套记忆,且没有区分 namespace | 检查长期记忆的写入来源 | 用 task_id + namespace 隔离记忆 |
| 工具调用越权 | 权限配置只写在业务代码里 | 检查工具调用日志中是否有非预期调用 | 把权限收口到控制平面 |
| 定位问题靠猜 | 没有任务级 trace | 看有没有 trace 平台 | 优先补可观测性,再谈优化 |
| 换框架就要改核心逻辑 | 基建绑定了产品形态 | 检查运行时层是否依赖具体框架 SDK | 抽象出运行时层和框架解耦 |
这些问题里,最值得提醒的是“上下文溢出”。很多人以为 Agent 只要模型够大就能记住一切,实际上上下文窗口再大也有上限。更合理的做法是让 Agent 养成“主动裁剪”的习惯,把对话历史摘要化,只保留当前任务的关键信息。
10. 落地原则与意见
最后还是回到标题的问题:Infra 到底该为谁而建?
我的答案是:不是为今天某个 Agent 形态而建,而是为 Agent 系统里那些“无论形态怎么变都不变的结构”而建。这些结构包括执行循环、上下文管理、工具注册表、记忆接口、控制平面、可观测链路。这些是 Agent 系统真正的骨架,也是基础设施的合理边界。
在建设新的 Agent Infra 时,你始终可以问自己一个问题:如果明天 Agent 的形态换了一个新的外壳,我这套东西还剩下多少能用?剩得越多,说明你建得越对;剩得越少,说明你只是把框架的插件写成了自己的代码。
最值得先动手的,不是设计完美的框架,而是把“循环 + 工具注册 + 日志”这个最小闭环跑通。有了它,后续的形态变化,你都能接得住。