Back to Journal
02 / Entry· 7 min read

从零构建 Agent Loop:从一个 while 循环到生产级 ReAct 框架

从最简单的 while 循环开始,逐步推演出生产级 Agent 框架的完整 Loop 架构,最终以 AgentScope 2.0 为蓝本拆解其工程实现。

🔊 系统朗读

引言:Agent 的核心就是一个"循环"

如果你拆开所有 Agent 框架的外壳——LangChain、AutoGPT、CrewAI、AgentScope——你会发现它们的内核惊人地相似:一个不断在"思考"和"行动"之间切换的循环

这篇文章不会一上来就扔给你一个复杂的框架源码。我们会从一个 20 行的 Python 脚本开始,每解决一个真实问题就加一层,最终你会自然地理解像 AgentScope 这样的生产级框架为什么要那样设计。


第一层:最小可运行的 Agent Loop

忘掉所有框架。一个 Agent 的本质就是:

python
def agent_loop(user_input: str):
    messages = [
        {"role": "system", "content": "你是一个助手,可以调用工具。"},
        {"role": "user", "content": user_input},
    ]

    while True:
        response = call_llm(messages, tools=available_tools)

        if response.has_tool_calls():
            # 有工具调用 → 执行工具,把结果追加到上下文,继续循环
            for tool_call in response.tool_calls:
                result = execute_tool(tool_call)
                messages.append({"role": "tool", "content": result})
        else:
            # 没有工具调用 → 模型认为可以直接回答了
            return response.text

这就是全部。 一个 while True,两个分支:要么调工具继续转,要么给出最终回答退出。

画出来就是这么简单:

用户输入 → [LLM 思考] ──有工具调用──→ [执行工具] ──→ 回到 LLM
                │
                └──无工具调用──→ 输出最终回答 → 结束

这个循环有个名字,叫 ReAct(Reasoning + Acting),2022 年由 Yao et al. 提出。几乎所有现代 Agent 框架都是它的变体。

这一层的问题

它能跑,但在生产环境里会立刻撞上几面墙:

  1. 死循环:如果模型一直调工具不给最终回答怎么办?
  2. 没有流式输出:用户只能干等完整回答
  3. 工具出错怎么办:异常直接炸掉整个循环
  4. 上下文爆炸:对话越来越长,token 烧到爆
  5. 没有权限控制:模型说删库就删库?

下面我们一个一个解决。


第二层:给循环装上"刹车"

死循环的解法很朴素——加一个迭代上限:

python
MAX_ITERS = 20

def agent_loop(user_input: str):
    messages = build_initial_messages(user_input)

    for cur_iter in range(MAX_ITERS):
        response = call_llm(messages, tools=available_tools)

        if not response.has_tool_calls():
            return response.text  # 正常结束

        for tool_call in response.tool_calls:
            result = execute_tool(tool_call)
            messages.append({"role": "tool", "content": result})

    # 超限了
    return "抱歉,我尝试了很多次但没能完成任务。"

别小看这几行改动——它引入了一个核心概念:循环决策(Loop Decision)

每次迭代结束时,循环需要做一个三选一的决策:

决策含义
继续推理(Reasoning)还需要再想想,再调一次 LLM
执行动作(Acting)LLM 决定调工具,去执行
退出(Exit)有了最终回答,或者超出限制

这个"三选一"的模式,在后面 AgentScope 的实现中会变得更加精细和强大。


第三层:流式事件——让 Agent "直播"自己的思考

while 循环加 return 的模式有个问题:它是"黑盒"的。用户提交请求后只能干等,看不到中间过程。

解法是把 return 改成 yield,让循环变成一个事件流生成器

python
from enum import Enum

class EventType(Enum):
    THINKING = "thinking"       # 模型正在思考
    TEXT_DELTA = "text_delta"   # 文本片段
    TOOL_CALL = "tool_call"     # 工具调用开始
    TOOL_RESULT = "tool_result" # 工具返回结果
    REPLY_END = "reply_end"     # 最终回答完成

def agent_loop_stream(user_input: str):
    messages = build_initial_messages(user_input)

    for cur_iter in range(MAX_ITERS):
        yield {"type": EventType.THINKING, "iter": cur_iter}

        response = call_llm(messages, tools=available_tools)

        if not response.has_tool_calls():
            # 流式输出最终文本
            for chunk in response.stream_text():
                yield {"type": EventType.TEXT_DELTA, "text": chunk}
            yield {"type": EventType.REPLY_END}
            return

        for tool_call in response.tool_calls:
            yield {"type": EventType.TOOL_CALL, "name": tool_call.name}
            result = execute_tool(tool_call)
            yield {"type": EventType.TOOL_RESULT, "output": result}
            messages.append({"role": "tool", "content": result})

现在前端可以实时展示:Agent 正在思考什么、调了什么工具、拿到了什么结果。这就是 AgentScope 2.0 中 AsyncGenerator[AgentEvent] 的雏形。


第四层:工具系统——不只是函数调用

从裸函数到工具协议

真实的 Agent 不会只调几个裸函数。工具需要:

  • 描述(让 LLM 知道这工具干嘛的)
  • 输入 Schema(让 LLM 知道该传什么参数)
  • 执行函数(实际逻辑)
  • 元信息(是否安全、是否只读、能否并发)
python
class Tool:
    name: str
    description: str
    input_schema: dict          # JSON Schema
    is_read_only: bool          # 只读工具(如搜索)→ 可以自动批准
    is_concurrency_safe: bool   # 并发安全 → 可以批量执行

    async def call(self, **kwargs) -> str:
        raise NotImplementedError

工具的并发策略

当 LLM 一次性返回多个 tool call 时,一个关键决策出现了:串行还是并行?

python
# AgentScope 的策略:根据工具属性自动决定
safe_calls = [c for c in tool_calls if tool.is_concurrency_safe]
unsafe_calls = [c for c in tool_calls if not tool.is_concurrency_safe]

# 安全的并发执行
safe_results = await asyncio.gather(*[tool.call(**c.args) for c in safe_calls])

# 不安全的顺序执行
for call in unsafe_calls:
    result = await tool.call(**call.args)

这个设计在 AgentScope 中被显式建模——每个工具都要声明 is_concurrency_safe 属性。

工具组与动态切换

更进一步的场景:Agent 可能有几十个工具,但不应该每次都把所有工具的 schema 都塞给 LLM(太长的工具列表会降低推理质量)。

AgentScope 的做法是引入 ToolGroup(工具组):

Toolkit
├── basic 组(始终可用): search, calculator, datetime
├── code 组(按需激活): run_python, run_shell, read_file
└── data 组(按需激活): query_db, plot_chart, export_csv

Agent 可以通过一个特殊的"元工具"(reset_tools)来动态切换工具组。这是 Agent 自己管理自己的能力范围——一种非常 Agentic 的设计。


第五层:中间件——给循环的每个环节"装拦截器"

随着系统变复杂,你会发现需要在循环的各个点插入横切逻辑:

  • 推理前:记录日志、注入额外上下文
  • 推理后:过滤敏感内容、token 统计
  • 工具调用前:权限校验、速率限制
  • 工具调用后:结果脱敏、审计记录

如果在循环里硬编码这些逻辑,代码会变成意大利面。更好的方案是中间件模式——借鉴 Web 框架的 middleware 思路。

洋葱模型

请求进入 →
  [Middleware A: before] →
    [Middleware B: before] →
      [核心逻辑]
    [Middleware B: after] →
  [Middleware A: after] →
← 响应返回

AgentScope 的中间件系统有 7 个 hook 点,覆盖了 Agent 生命周期的每个关键阶段:

python
class MiddlewareBase:
    async def on_reply(self, agent, input_kwargs, next_handler):
        """拦截整个 reply 过程"""
        async for event in next_handler():
            yield event

    async def on_reasoning(self, agent, input_kwargs, next_handler):
        """拦截推理阶段"""
        async for event in next_handler():
            yield event

    async def on_acting(self, agent, input_kwargs, next_handler):
        """拦截工具执行"""
        async for event in next_handler():
            yield event

    async def on_check_permission(self, agent, input_kwargs, next_handler):
        """拦截权限检查"""
        async for event in next_handler():
            yield event

    # ... 还有 on_model_call, on_compress_context, on_system_prompt

使用示例——写一个日志中间件:

python
class LoggingMiddleware(MiddlewareBase):
    async def on_reasoning(self, agent, input_kwargs, next_handler):
        logger.info(f"[{agent.name}] 开始推理...")
        async for event in next_handler():
            yield event
        logger.info(f"[{agent.name}] 推理完成")

    async def on_acting(self, agent, input_kwargs, next_handler):
        logger.info(f"[{agent.name}] 执行工具...")
        async for event in next_handler():
            yield event

关键设计:Agent 在初始化时自动扫描每个 middleware 实现了哪些 hook,按 hook 分组存储。这意味着你不需要显式注册——写了方法就自动生效。


第六层:Human-in-the-Loop——循环可以"暂停"

有些工具调用太危险了(比如删数据、发邮件),不能自动执行。我们需要循环能够暂停、等待人类确认、然后恢复

这在实现上是一个有趣的问题:while 循环本身不支持"暂停"。

AgentScope 的方案:状态机 + 事件驱动

Tool call 被建模为一个状态机

PENDING → ASKING(等待确认)→ ALLOWED → FINISHED
                            → DENIED

当权限检查返回 ASK 时,循环不是阻塞等待,而是 yield 一个特殊事件然后退出当前循环

python
async def _execute_tool_call(self, tool_call):
    permission = await self._check_permission(tool_call)

    if permission == "ASK":
        # 暂停!yield 一个事件告诉外部"我需要确认"
        yield RequireUserConfirmEvent(tool_call=tool_call)
        return  # 退出当前循环,状态保存

    elif permission == "DENY":
        yield ToolResult(state="DENIED", error="权限不足")

    elif permission == "ALLOW":
        result = await self.toolkit.call_tool(tool_call)
        yield ToolResult(state="FINISHED", output=result)

当用户在前端点击"允许"后,系统会带着确认结果重新进入 reply_stream(),Agent 检测到有 HITL 恢复事件,从暂停的地方继续执行。

这种设计让 Agent 的循环不是一个封闭的 while,而是一个可中断、可恢复的状态机


第七层:上下文管理——防止 Token 爆炸

循环每转一圈,messages 列表就变长一点。工具返回的结果可能很长(比如一个网页搜索返回几千字)。不控制的话,几轮之后 token 就爆了。

策略一:工具结果截断

python
MAX_TOOL_RESULT = 4000  # 字符

def truncate_tool_result(result: str, limit: int = MAX_TOOL_RESULT) -> str:
    if len(result) <= limit:
        return result
    return result[:limit] + f"\n... [已截断,原始长度 {len(result)} 字符]"

策略二:上下文压缩

当上下文长度接近模型窗口上限时,自动触发压缩——用 LLM 对早期消息做摘要:

python
class ContextConfig:
    trigger_ratio: float = 0.8    # 上下文使用率超过 80% 时触发
    reserve_ratio: float = 0.1    # 压缩后保留 10% 的空间

async def compress_context(self, messages):
    """当上下文过长时,对早期消息做摘要"""
    # 保留最近 N 条消息不动
    # 对更早的消息调用 LLM 生成摘要
    # 用摘要替换原始消息
    return compressed_messages

策略三:运行时状态注入

与其把所有信息都放在 system prompt 里,不如在每次推理前动态注入当前状态:

python
class InjectionConfig:
    inject_time: bool = True         # 注入当前时间
    inject_task_state: bool = True   # 注入任务进度
    inject_context_length: bool = True # 注入上下文长度感知

这让 Agent 能够"感知"自己的运行状态,比如"你已经用了 15 轮迭代,还剩 5 轮"——帮助 LLM 做出更合理的决策。


第八层:完整的 ReAct 循环——AgentScope 的实现

现在我们已经积累了足够的理解,来看 AgentScope 2.0 的完整实现。它把我们上面讨论的所有东西整合成了一个协调的系统。

核心架构一览

reply_stream(user_input)
  │
  ├── 1. 检查输入事件(新消息 / HITL 恢复)
  ├── 2. 构建 ReplyContext(中间件链、状态、上下文)
  │
  └── 3. ┌──── ReAct 主循环 ────────────────────────────┐
          │                                               │
          │  while True:                                  │
          │    action = _next_action()   ← 决策器         │
          │                                               │
          │    if action == Exit:                         │
          │      yield ReplyEndEvent → return             │
          │                                               │
          │    if action == Reasoning:                    │
          │      async for event in _reasoning():         │
          │        yield event     ← 流式推理事件          │
          │                                               │
          │    if action == Acting:                       │
          │      async for event in _batch_tool_calls():  │
          │        yield event     ← 流式工具执行事件       │
          │                                               │
          │    cur_iter += 1                              │
          └───────────────────────────────────────────────┘

决策器:_next_action

这是整个循环的"大脑",一个只读方法——它不修改任何状态,只返回决策:

python
def _next_action(self) -> Reasoning | Acting | Exit:
    # 1. 有待执行的 tool calls(已授权)→ Acting
    if self._has_pending_tool_calls():
        return Acting()

    # 2. 有等待确认/外部执行的 tool calls → Exit(暂停)
    if self._has_waiting_tool_calls():
        return Exit(reason="waiting_for_confirmation")

    # 3. 结构化输出检查 → 完成则 Exit,否则继续 Reasoning
    if self._needs_structured_output():
        if self._structured_output_complete():
            return Exit(reason="structured_output_done")
        return Reasoning(hint="请输出结构化数据", tool_choice="required")

    # 4. 已有最终文本回复 → Exit
    if self._has_final_message():
        return Exit(reason="final_message")

    # 5. 超出迭代上限 → Exit
    if self.cur_iter >= self.max_iters:
        return Exit(reason="exceed_max_iters")

    # 6. 默认 → 继续推理
    return Reasoning()

三种返回值对应三条路径,清晰且互斥。这是经典的策略模式

推理阶段:_reasoning

python
async def _reasoning(self, tool_choice=None):
    # 1. 准备模型输入(messages + tools schema + 状态注入)
    model_input = self._prepare_model_input(tool_choice)

    # 2. 调用 LLM(经过 middleware 链包装)
    async for chunk in self._call_model_through_middleware(model_input):
        # 3. 流式处理每个 chunk → 转换为事件
        if chunk.is_text:
            yield TextBlockDeltaEvent(text=chunk.text)
        elif chunk.is_thinking:
            yield ThinkingBlockDeltaEvent(text=chunk.text)
        elif chunk.is_tool_call:
            yield ToolCallStartEvent(tool_call=chunk.tool_call)

    # 4. 保存到上下文
    self._save_to_context(response)

工具执行阶段:_batch_tool_calls

python
async def _batch_tool_calls(self):
    tool_calls = self._get_pending_tool_calls()

    # 按并发安全性分组
    concurrent = [c for c in tool_calls if c.tool.is_concurrency_safe]
    sequential = [c for c in tool_calls if not c.tool.is_concurrency_safe]

    # 并发执行安全的工具
    if concurrent:
        tasks = [self._execute_tool_call(c) for c in concurrent]
        async for event in merge_async_generators(*tasks):
            yield event

    # 顺序执行不安全的工具
    for call in sequential:
        async for event in self._execute_tool_call(call):
            yield event

事件系统:25 种事件类型

AgentScope 定义了约 25 种事件类型,覆盖 Agent 行为的每个维度:

分类事件说明
生命周期REPLY_START, REPLY_END一次完整的回复过程
推理TEXT_BLOCK_START/DELTA/END文本流式生成
思考THINKING_BLOCK_START/DELTA/END模型思考过程(如 CoT)
工具TOOL_CALL_START/DELTA/END工具调用流
结果TOOL_RESULT_START/TEXT_DELTA/END工具结果流
交互REQUIRE_USER_CONFIRM, USER_CONFIRM_RESULTHITL 暂停/恢复
控制EXCEED_MAX_ITERS, USER_INTERRUPT异常终止

这种设计让前端可以精确地渲染 Agent 的每一步行为——不是简单的"loading...done",而是一个实时的、可观察的思考过程。


第九层:多 Agent 编排——不用 Pipeline,用 Agent 编排 Agent

传统的 Agent 框架喜欢用 Pipeline/DAG 来编排多个 Agent:

python
# 传统方式:显式编排
pipeline = SequentialPipeline([
    ResearchAgent(),
    WritingAgent(),
    ReviewAgent(),
])
pipeline.run(input)

AgentScope 2.0 有意放弃了这种方式。它的哲学是:

让 LLM 自己决定该怎么协作,而不是用硬编码的流程约束它。

Leader-Worker 模式

取而代之的是 Leader Agent + Team Tools

python
# Leader Agent 的 toolkit 里有特殊的"团队工具"
leader = Agent(
    name="项目经理",
    system_prompt="你是一个项目经理,负责协调团队完成任务...",
    toolkit=Toolkit(tools=[
        spawn_agent_tool,      # 创建一个 Worker Agent
        assign_task_tool,      # 给 Worker 分配任务
        collect_result_tool,   # 收集 Worker 的结果
    ]),
)

Leader 本身就是一个普通的 Agent,运行普通的 ReAct 循环。但它通过工具调用来"创建"和"指挥" Worker Agent。条件分支、循环、并行——这些编排逻辑不再写在代码里,而是由 LLM 在推理过程中自然决定。

跨 Agent 通信:Message Bus

当多个 Agent 需要跨进程/跨机器协作时,AgentScope 提供了三种消息模式:

模式语义适用场景
Queue(队列)单消费者,读后即删任务分发,确保只被一个 Worker 处理
Log(日志)多消费者,游标独立事件溯源,多个 Agent 观察同一事件流
Pub/Sub(发布订阅)fire-and-forget状态广播,Agent 上下线通知

后端可以是进程内(InMemoryMessageBus)或分布式(RedisMessageBus)。


全景回顾:从 20 行到 20000 行

让我们回顾一下这段旅程,看看每一层解决了什么问题:

第1层  while True 循环          → 最小可用的 ReAct
第2层  迭代上限                 → 防止死循环(循环决策的雏形)
第3层  yield 事件流             → 流式输出,实时可观察
第4层  工具协议 + 并发策略       → 从裸函数到标准化工具系统
第5层  中间件(洋葱模式)        → 横切逻辑的优雅解耦
第6层  HITL 状态机              → 可中断、可恢复的循环
第7层  上下文管理               → 防止 token 爆炸
第8层  整合为完整 ReAct 系统     → 决策器 + 推理 + 执行 + 事件流
第9层  多 Agent 编排            → Leader-Worker + Message Bus

每一层都不是凭空设计的——它们是在解决上一层暴露出的真实问题。这就是为什么 AgentScope 的代码有那么多"看起来多余"的抽象:它们在处理你在 demo 里遇不到、但在生产环境中必然遇到的问题。

设计模式速查表

设计模式在 Agent Loop 中的应用
ReAct 循环核心——Reasoning ↔ Acting 交替执行
策略模式_next_action() 三选一决策
洋葱中间件7 个 hook 点的拦截链
状态机ToolCall 生命周期管理
事件流/生成器AsyncGenerator[AgentEvent] 驱动前端
发布/订阅Message Bus 跨 Agent 通信
组合模式ContentBlock 多态消息内容

与传统 Pipeline 框架的哲学分歧

这是 AgentScope 最值得思考的设计决策。传统框架(包括 AgentScope 自己的 1.0 版本)用显式的 SequentialPipelineIfElsePipelineWhileLoopPipeline 来编排流程。而 2.0 版本认为:

当 LLM 足够聪明时,流程控制应该由 LLM 在推理过程中完成,而不是硬编码在代码里。

这不是说 Pipeline 没有价值——对于确定性强、流程固定的场景(如 ETL 管道),Pipeline 仍然是更好的选择。但对于需要灵活决策的 Agent 场景,让 LLM 自己掌握"下一步做什么"是更自然的设计。


动手建议

如果你想真正理解这些概念,建议按以下顺序实践:

  1. 先写第 1 层的 20 行代码,用 OpenAI API + 一个计算器工具跑通
  2. 加上迭代上限和日志,体会循环决策
  3. 改成流式输出,体会事件驱动
  4. 加一个简单的中间件(比如计时),体会洋葱模式
  5. 加权限检查(比如危险工具需要 input() 确认),体会 HITL
  6. 然后再去看 AgentScope 的源码,你会发现那些复杂的抽象不再神秘

Agent 框架的核心从来不是那些花哨的功能——就是一个循环,加上一圈精心设计的护栏

分享
← 返回博客列表
🎁 有邀请福利哦,点击查看
🎁