引言:Agent 的核心就是一个"循环"
如果你拆开所有 Agent 框架的外壳——LangChain、AutoGPT、CrewAI、AgentScope——你会发现它们的内核惊人地相似:一个不断在"思考"和"行动"之间切换的循环。
这篇文章不会一上来就扔给你一个复杂的框架源码。我们会从一个 20 行的 Python 脚本开始,每解决一个真实问题就加一层,最终你会自然地理解像 AgentScope 这样的生产级框架为什么要那样设计。
第一层:最小可运行的 Agent Loop
忘掉所有框架。一个 Agent 的本质就是:
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 框架都是它的变体。
这一层的问题
它能跑,但在生产环境里会立刻撞上几面墙:
- 死循环:如果模型一直调工具不给最终回答怎么办?
- 没有流式输出:用户只能干等完整回答
- 工具出错怎么办:异常直接炸掉整个循环
- 上下文爆炸:对话越来越长,token 烧到爆
- 没有权限控制:模型说删库就删库?
下面我们一个一个解决。
第二层:给循环装上"刹车"
死循环的解法很朴素——加一个迭代上限:
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,让循环变成一个事件流生成器:
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 知道该传什么参数)
- 执行函数(实际逻辑)
- 元信息(是否安全、是否只读、能否并发)
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 时,一个关键决策出现了:串行还是并行?
# 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 生命周期的每个关键阶段:
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
使用示例——写一个日志中间件:
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 一个特殊事件然后退出当前循环:
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 就爆了。
策略一:工具结果截断
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 对早期消息做摘要:
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 里,不如在每次推理前动态注入当前状态:
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
这是整个循环的"大脑",一个只读方法——它不修改任何状态,只返回决策:
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
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
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_RESULT | HITL 暂停/恢复 |
| 控制 | EXCEED_MAX_ITERS, USER_INTERRUPT | 异常终止 |
这种设计让前端可以精确地渲染 Agent 的每一步行为——不是简单的"loading...done",而是一个实时的、可观察的思考过程。
第九层:多 Agent 编排——不用 Pipeline,用 Agent 编排 Agent
传统的 Agent 框架喜欢用 Pipeline/DAG 来编排多个 Agent:
# 传统方式:显式编排
pipeline = SequentialPipeline([
ResearchAgent(),
WritingAgent(),
ReviewAgent(),
])
pipeline.run(input)
AgentScope 2.0 有意放弃了这种方式。它的哲学是:
让 LLM 自己决定该怎么协作,而不是用硬编码的流程约束它。
Leader-Worker 模式
取而代之的是 Leader Agent + Team Tools:
# 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 版本)用显式的 SequentialPipeline、IfElsePipeline、WhileLoopPipeline 来编排流程。而 2.0 版本认为:
当 LLM 足够聪明时,流程控制应该由 LLM 在推理过程中完成,而不是硬编码在代码里。
这不是说 Pipeline 没有价值——对于确定性强、流程固定的场景(如 ETL 管道),Pipeline 仍然是更好的选择。但对于需要灵活决策的 Agent 场景,让 LLM 自己掌握"下一步做什么"是更自然的设计。
动手建议
如果你想真正理解这些概念,建议按以下顺序实践:
- 先写第 1 层的 20 行代码,用 OpenAI API + 一个计算器工具跑通
- 加上迭代上限和日志,体会循环决策
- 改成流式输出,体会事件驱动
- 加一个简单的中间件(比如计时),体会洋葱模式
- 加权限检查(比如危险工具需要
input()确认),体会 HITL - 然后再去看 AgentScope 的源码,你会发现那些复杂的抽象不再神秘
Agent 框架的核心从来不是那些花哨的功能——就是一个循环,加上一圈精心设计的护栏。
