Agent Runtime 九个关键设计:状态外化、上下文压缩与多智能体协同

把 Agent 从能跑到可靠,关键不在模型神准,而在状态、上下文和协作工程。 原文链接AI 小老六

聊 Agent 时,很多讨论容易落到模型能力上:模型会不会推理,代码写得准不准,能不能理解复杂需求。这些当然重要,但​真正把 Agent 做到可用以后​,会发现麻烦往往不在"想不明白",而在"执行过程中会散"。

模型可以给出不错的判断,却不会天然知道自己刚才读过哪些文件;它可以规划十步任务,却可能在第五步被一段日志带跑;它能调用工具,但每一次工具返回的内容都会塞进上下文,越做越重。再往后,如果要让多个 Agent 一起做事,还会冒出通信、审批、退出、状态同步这些更琐碎的问题。

所以我更愿意把 Agent 看成一个​状态工程问题​。模型负责判断,系统负责让判断落地,并且让落地过程可追踪、可恢复、可约束。

下面不是按功能清单罗列,而是按一个 Agent 从"能跑起来"到"能长期干活"的过程,把几个关键设计放在一起讲。 图:可靠 Agent 的核心不是单次推理,而是可追踪、可恢复的状态工程。

Tool-Calling Runtime​:把推理闭环接入真实执行环境

最原始的模型只是一个文本系统。你把代码贴进去,它能分析;你把日志贴进去,它能猜根因。但它不会自己打开 src/auth.js,不会自己跑 npm test,也不会知道当前目录下到底有什么。

Agent Loop 解决的是这个入口问题:模型决定要不要调用工具,Harness 执行工具,再把结果交回给模型。这个循环一直持续到模型不再请求工具。

图:Agent Loop 将模型判断、工具执行和结果回写串成闭环

一个极简实现大概长这样:

python 复制代码
messages = [{"role": "user", "content": query}]

while True:
    response = client.messages.create(
        model=MODEL,
        system=SYSTEM,
        messages=messages,
        tools=TOOLS,
        max_tokens=8000,
    )

    messages.append({"role": "assistant", "content": response.content})

    if response.stop_reason != "tool_use":
        return response.content

    results = []
    for block in response.content:
        if block.type != "tool_use":
            continue
        output = run_tool(name=block.name, input=block.input)
        results.append({
            "type": "tool_result",
            "tool_use_id": block.id,
            "content": output,
        })

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

这里有个分工很重要:​模型只做选择,Harness 负责执行​。工具层必须有自己的安全边界,不能指望模型每次都谨慎。

比如一个 Bash 工具,至少要拦掉明显危险的命令,限制路径逃逸,避免 shell=True 带来的注入风险。

python 复制代码
import os
import subprocess


def run_bash_simple_secure(command: str) -> str:
    dangerous = ["rm", "sudo", "shutdown", "reboot", ">", "|", "&", ";"]
    if any(part in dangerous for part in command.split()):
        return "Error: dangerous command blocked."

    if "../" in command or command.startswith("/"):
        return "Error: path must stay inside workspace."

    try:
        args = command.split()
        result = subprocess.run(
            args,
            cwd=os.getcwd(),
            capture_output=True,
            text=True,
            timeout=10,
            shell=False,
        )
        text = (result.stdout + result.stderr).strip()
        return text[:50000] if text else "(no output)"
    except FileNotFoundError:
        return f"Error: command '{args[0]}' not found."
    except Exception as e:
        return f"Error: {e}"

工具也别做成一个大杂烩。读文件、写文件、编辑文件、跑命令,最好各管各的。Loop 里只需要一个分发表,把模型说出的工具名映射到真实处理函数。

python 复制代码
TOOL_HANDLERS = {
    "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")),
    "write_file": lambda **kw: run_write(kw["path"], kw["content"]),
    "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]),
}

到这一步,Agent 算是有了眼睛和手。但能动手不代表能把长任务做稳。

Progress Externalization​:用外部状态锁住长任务进度

短任务里,模型的表现通常不错。让它读一个文件、解释一个报错、改一个小函数,来回几轮就能收尾。

麻烦出现在多步任务里。比如一次重构要做十件事:先读模块,再设计接口,再改实现,再补测试,再跑验证。前几步通常没问题,后面就开始漂。它可能重复读已经读过的文件,也可能跳过前置设计,直接改代码;更常见的是被某个局部问题吸住,忘了用户真正要的是什么。

这不是"模型笨",而是任务状态只放在上下文里太脆弱。上下文会被日志、代码、报错和解释不断稀释。

TodoManager 的作用很朴素:​把进度写到外面​。不要让模型靠记忆维护计划。

设计点 为什么需要
pending / in_progress / completed 让任务进度显式化
同一时间只允许一个 in_progress 避免模型同时推进多件互相冲突的事
最多 20 个任务 防止拆任务本身变成噪声
可渲染文本 让模型每次都能快速读懂当前局面

一个简化版实现如下:

python 复制代码
from typing import Dict, List


class TodoManager:
    MAX_ITEMS = 20
    VALID_STATUSES = ("pending", "in_progress", "completed")
    MARKERS = {
        "pending": "[ ]",
        "in_progress": "[>]",
        "completed": "[x]",
    }

    def __init__(self) -> None:
        self.items: List[Dict[str, str]] = []

    def update(self, items: List[Dict[str, str]]) -> str:
        if len(items) > self.MAX_ITEMS:
            raise ValueError(f"Max {self.MAX_ITEMS} todos allowed")

        in_progress = 0
        checked: List[Dict[str, str]] = []

        for i, item in enumerate(items):
            text = str(item.get("text", "")).strip()
            status = str(item.get("status", "pending")).lower()
            item_id = str(item.get("id", i + 1))

            if not text:
                raise ValueError(f"Item {item_id}: text required")
            if status not in self.VALID_STATUSES:
                raise ValueError(f"Item {item_id}: invalid status '{status}'")
            if status == "in_progress":
                in_progress += 1

            checked.append({"id": item_id, "text": text, "status": status})

        if in_progress > 1:
            raise ValueError("Only one task can be in_progress at a time")

        self.items = checked
        return self.render()

    def render(self) -> str:
        if not self.items:
            return "No todos."

        lines = []
        for item in self.items:
            lines.append(f"{self.MARKERS[item['status']]} #{item['id']}: {item['text']}")

        done = sum(1 for item in self.items if item["status"] == "completed")
        lines.append(f"\n({done}/{len(self.items)} completed)")
        return "\n".join(lines)

光有 todo 还不够。模型有时会连续几轮不看清单。Nag Reminder 就是一个轻量提醒器:如果模型太久没碰 todo,就在下一次工具结果里塞一行提醒。

它不强行改模型动作,只是把注意力拽回来一下。

python 复制代码
NAG_THRESHOLD = 3
NAG_TEXT = "<reminder>Update your todos.</reminder>"
TODO_TOOL = "todo"


def agent_loop(messages):
    rounds_since_todo = 0

    while True:
        response = client.messages.create(
            model=MODEL,
            system=SYSTEM,
            messages=messages,
            tools=TOOLS,
            max_tokens=8000,
        )
        messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            return

        tool_uses = [b for b in response.content if b.type == "tool_use"]
        results = []

        for block in tool_uses:
            output = dispatch_tool(block.name, block.input)
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": output,
            })

        used_todo = any(block.name == TODO_TOOL for block in tool_uses)
        rounds_since_todo = 0 if used_todo else rounds_since_todo + 1

        if rounds_since_todo >= NAG_THRESHOLD:
            results.append({"type": "text", "text": NAG_TEXT})
            rounds_since_todo = 0

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

DAG Task System:从待办清单升级到依赖调度

TodoManager 解决的是"别忘事"。但复杂工程不只是很多事项并列放着,它们之间有先后关系。

接口没定,调用方就不该改;数据结构没定,存储层就不该写死;基础能力没完成,集成测试跑了也只是浪费时间。扁平清单没法表达这些约束。

这时任务系统要从 list 变成 ​DAG​。

图:复杂工程任务从扁平清单升级为依赖图

一种直接的做法是把每个任务存成独立 JSON 文件,放在 .tasks/ 目录里。

复制代码
.tasks/
├── task_1.json
├── task_2.json
└── task_3.json

任务字段不用复杂,但要足够表达依赖。

字段 含义
id 任务唯一标识
title 任务标题
description 更完整的任务说明
status pending / in_progress / completed
blockedBy 当前任务依赖哪些前置任务
createdAt 创建时间
completedAt 完成时间

Agent 每次只需要回答三个问题。

问题 判断方式
现在能做什么 status=pending
blockedBy=[]
什么还不能做 blockedBy
非空
完成后影响谁 找出依赖当前任务的后续任务

任务完成时,系统把它的 ID 从其他任务的 blockedBy 里删掉。如果某个任务的 blockedBy 变成空数组,它就解锁了。

图:前置任务完成后,系统自动释放可执行任务

这个变化不显眼,但很关键。它把"模型凭感觉挑下一步",改成了"系统告诉模型哪些步骤已经具备前置条件"。

Context Compaction​:控制 Token 膨胀和信息半衰期

图:把长历史压缩成摘要,让主上下文只保留当前真正需要的信息。

Agent Loop 有个副作用:工具结果会持续进入上下文。读文件、跑命令、查日志、搜索代码,每一次都可能带来几千 tokens。几轮下来,主上下文很快就变成垃圾堆。

这时会出现两个问题。第一,模型注意力被旧信息稀释;第二,真正有用的远端信息可能被截断或召回不到。

粗略看一次工程任务的上下文消耗:

操作 数量 估算消耗
读取源码文件 30 个 约 60,000 tokens
执行 Shell 命令 20 条 约 30,000 tokens
工具调用往返 15 次 约 20,000 tokens
模型输出 多轮持续生成 约 15,000 tokens

这就是为什么需要 compact。​压缩不是删除历史​,而是把历史从"模型当前必须看见的内容"里移出去。

层次 做什么 类比
micro_compact 把几轮前的工具结果换成占位符 页面置换
auto_compact 上下文超阈值后,保存全文并摘要替换 Swap
compact
工具 模型主动触发压缩 手动 GC

micro_compact 最简单。超过 3 轮的旧工具结果,保留调用痕迹,删掉大块输出。

csharp 复制代码
[Previous: used read_file]
[Previous: used bash]

压缩收益很大:一次 2000 tokens 的文件读取结果,可能只剩十来个 tokens。模型仍知道自己"读过文件",但不会每轮都背着全文走。

auto_compact 再激进一点。上下文超阈值时,系统先把完整 transcript 写入磁盘,再让模型生成任务摘要,用摘要替代活跃历史。

yaml 复制代码
.transcripts/
├── 2026-05-14_task-refactor-auth-module.md
├── 2026-05-14_task-fix-api-endpoint.md
└── 2026-05-13_task-setup-ci-pipeline.md

摘要应该保留用户目标、关键决策、当前状态和剩余事项。日志全文、试错过程、已经过期的文件内容,都不该继续占着窗口。

还有一种情况适合让模型自己调用 compact:任务进入新阶段,或者它预判接下来要读很多文件。这时主动清理,比等系统阈值触发更稳。

图:compact 将完整历史归档,并把摘要留在活跃上下文

Subagent Isolation:隔离高噪声探索链路

并不是所有工作都适合主 Agent 亲自做。比如排查一个测试失败,可能要读很多文件、跑很多命令、试几个假设。主流程真正需要的是最后的结论:问题在哪,证据是什么,建议怎么修。

Subagent 就是为这种场景准备的​。它拿到一个子任务,在独立上下文里完成探索,然后只把结论交还给主 Agent。中间那些日志、搜索结果、错误尝试,不进入主上下文。

图:子 Agent 在独立上下文中探索,只把结论交还主流程

实现上有几个约束值得保留:子 Agent 不继承主对话;子 Agent 没有再派生子 Agent 的工具;最多运行固定轮次;返回值只取最终文本。

python 复制代码
CHILD_TOOLS = [bash, read_file, write_file, edit_file]
PARENT_TOOLS = CHILD_TOOLS + [task]


def run_subagent(prompt: str) -> str:
    sub_messages = [{"role": "user", "content": prompt}]

    for _ in range(30):
        response = client.messages.create(
            model=MODEL,
            system=SUBAGENT_SYSTEM,
            messages=sub_messages,
            tools=CHILD_TOOLS,
            max_tokens=8000,
        )
        sub_messages.append({"role": "assistant", "content": response.content})

        if response.stop_reason != "tool_use":
            break

        results = []
        for block in response.content:
            if block.type != "tool_use":
                continue
            handler = TOOL_HANDLERS.get(block.name)
            output = handler(**block.input) if handler else f"unknown tool: {block.name}"
            results.append({
                "type": "tool_result",
                "tool_use_id": block.id,
                "content": str(output)[:50000],
            })

        sub_messages.append({"role": "user", "content": results})

    return "".join(
        block.text for block in response.content if hasattr(block, "text")
    ) or "(no summary)"

这个设计不是为了"多一个智能体显得高级",只是为了保护主上下文。主 Agent 像负责人,子 Agent 像临时调查员。负责人不需要看完整侦查录像,只需要拿到结论和证据。

Skill Injection​:让领域工作流按需进入上下文

Agent 往往要遵守很多工作流:Git 提交规范、单测规范、Code Review 清单、接口设计约定、文档写作模板。把这些都写进系统提示,看起来省事,实际很浪费。

假设一个 Skill 完整内容约 2000 tokens,10 个 Skill 全量注入就是 20000 tokens。而一次任务通常只会用到 1 到 2 个。

更好的方式是两层加载​。

第一层只放索引,让模型知道有哪些 Skill。

yaml 复制代码
Available Skills:

- git-conventions: Git 提交和分支命名规范
- test-patterns: 单元测试编写规范与最佳实践
- code-review: 代码审查清单与安全检查项
- api-design: RESTful API 设计规范
- docs-writing: 技术文档写作模板

第二层按需加载完整内容。

图:常驻索引与按需加载正文的两层 Skill 机制

这和工具类似:索引常驻,正文临时进入。上下文里只放当前有用的东西。

Background Execution​:把阻塞式工具调用改成异步任务

很多命令本身不需要模型参与。npm install 是下载依赖,pytest 是跑测试,docker build 是按 Dockerfile 构建镜像。它们可能跑很久,但中间不需要 Agent 做判断。

命令 典型耗时 主要工作
npm install 30-120 秒 下载和安装依赖
pytest 60-300 秒 执行测试套件
docker build 60-600 秒 构建镜像层

如果 Agent Loop 阻塞等待,模型的推理时间就被浪费了。后台任务管理器的做法是:​慢命令交给子进程跑​,Agent 立刻继续做别的事;命令结束后,结果进入队列,在下一次合适的调用边界注入。

图:慢命令进入后台执行,结果通过队列回到主循环

组件其实很少。

组件 作用
Agent 主循环 保持推理串行,避免并发推理混乱
守护线程或进程 盯着子进程结束
子进程 真正执行慢命令
通知队列 让执行和消费解耦

这个机制让 Agent 不必把时间花在"等"上。它可以一边跑测试,一边读下一组文件,或者把已经确定的代码先写掉。

Persistent Agent Teams:从临时子任务到稳定协作单元

图:稳定身份、消息通道和审批协议,让多个 Agent 像团队一样协同。

Subagent 适合临时探索,但它不是团队。真正的大任务需要长期协作:前端、后端、测试、文档各自有角色;每个人有自己的上下文和工具;任务可以异步分发和交付。

这时需要两个东西:​稳定身份和异步通信​。

一种够简单的通信方式是 JSONL 邮箱。每个 Agent 有一个 inbox 文件,发消息就是追加一行 JSON,收消息就是读取并清空自己的 inbox。

复制代码
.messages/
├── frontend-agent.inbox.jsonl
├── backend-agent.inbox.jsonl
└── test-agent.inbox.jsonl

消息格式保持结构化。

json 复制代码
{"from": "leader", "to": "frontend-agent", "timestamp": "2026-05-14T10:00:00Z", "type": "task", "content": "请实现登录页面的表单验证"}
{"from": "backend-agent", "to": "frontend-agent", "timestamp": "2026-05-14T10:05:00Z", "type": "info", "content": "API 接口已就绪,端点是 POST /api/login,参数为 {email, password}"}

JSONL 不花哨,但在这个场景里很合适。消息频率不高,文件足够快;内容可读,方便审计;进程挂了,消息也还在磁盘上。

方案 好处 问题
数据库 有事务 太重,要维护 schema
内存共享 进程崩溃就丢
JSONL 文件 简单、持久、人能看 性能不是最优

团队成员由 TeammateManager 管。它维护一个 config.json,记录每个成员的角色、技能和系统提示。

json 复制代码
{
  "teammates": [
    {
      "name": "frontend-agent",
      "role": "前端开发",
      "skills": ["react", "typescript", "css"],
      "system_prompt": "你是一个专注于前端开发的工程师..."
    },
    {
      "name": "backend-agent",
      "role": "后端开发",
      "skills": ["python", "fastapi", "postgresql"],
      "system_prompt": "你是一个专注于后端开发的工程师..."
    },
    {
      "name": "test-agent",
      "role": "质量保障",
      "skills": ["pytest", "e2e-testing", "ci-cd"],
      "system_prompt": "你是一个专注于测试的 QA 工程师..."
    }
  ]
}

spawn() 一个队友后,它有自己的上下文窗口、工具集和执行循环。主 Agent 不需要盯着它每一步,只要通过消息收发任务和结果。

Team Protocols:用请求-响应协议约束多 Agent 协作

多 Agent 之间能发消息以后,很快会遇到新的问题。

比如领导 Agent 想停掉一个队友,如果直接杀线程,可能文件写到一半、数据库连接没关、任务状态没更新。再比如某个队友想删除旧认证模块重写,这种高风险动作不应该没有确认就执行。还有消息格式,如果一会儿自然语言、一会儿 JSON、一会儿只发个 ok,接收方也会浪费推理能力猜意思。

所以团队需要协议​。

最小可用协议其实很简单:request-response 加一个三态状态机。A 发起请求,B 批准或拒绝。状态只有三个:pendingapprovedrejected

图:多 Agent 协作中的请求审批三态协议

它比 2PC、Raft、Pub/Sub 都轻得多,也更贴近 Agent 之间的真实交互。

协调方式 复杂度 适配度
两阶段提交 高,需要 prepare 和 commit 对 Agent 协作太重
Raft/Paxos 极高,需要投票和日志复制 不是这个问题
Pub/Sub 中等,需要 topic 管理 一对一请求用不上广播
Request-Response + 三态 FSM 低,两条消息就够 适合请求和审批

后续要加新协议也不难。只要定义新的 request type,状态机可以复用。

python 复制代码
"shutdown_request"  # pending -> approved/rejected
"plan_request"      # pending -> approved/rejected
"resource_request"  # pending -> approved/rejected
"merge_request"     # pending -> approved/rejected

如果一次交互需要补充信息,再加一个 needs_info 状态即可。

css 复制代码
[pending] -> [needs_info] -> [pending] -> [approved/rejected]

工程化收束:可靠 Agent 本质上是状态系统

Agent 工程没有想象中那么玄。很多设计看起来像"智能体架构",拆开以后其实都是老问题:状态怎么存,任务怎么排,日志怎么处理,慢任务怎么异步,团队怎么通信,高风险动作怎么审批。

模型确实带来了新的执行方式,但工程上的账不会消失。你不把状态显式化,状态就会散在上下文里;你不压缩历史,历史就会拖慢每一轮推理;你不定义协议,多 Agent 协作就会变成一群模型互相发散。

能用的 Agent,不是因为模型每一步都神准,而是因为系统允许它犯小错、能把它拉回任务、能记录它做过什么,也能在必要时让它停下来等一个确认。

相关推荐
AI_大白5 小时前
Codex 接入实时行情 MCP:从配置、鉴权到字段踩坑
后端·架构
plainGeekDev5 小时前
Android架构面试题:MVP/MVVM/MVI都分不清,架构师跟你没关系
面试·架构
oo哦哦5 小时前
深度解析:星链引擎全域智能营销矩阵系统的技术架构与实践
大数据·矩阵·架构
火山引擎开发者社区5 小时前
ArkClaw AI 持仓哨兵 —— 8 句话训练你的专属盯股助手
人工智能·agent
极品小學生7 小时前
拆解大模型时代的“流量交通枢纽”:API 中转站架构与核心原理
ai·架构·ai编程
KaneLogger7 小时前
从书架到浏览器,给 AI 接上了三个真实入口(WeRead、ima、kimi webbridge)
agent
uccs7 小时前
Agent循环原理
agent·ai编程·claude
AI观望者7 小时前
源码级拆解 Hermes Agent:记忆系统、上下文压缩与 MCP 集成的工程实现
人工智能·架构
情绪总是阴雨天~7 小时前
深度解析:LangChain、Agent、RAG、FC、ReAct、LangGraph、A2A、MCP — 区别、联系与全景图
python·langchain·agent·rag·langgraph·mcp·a2a