247 lines
6.4 KiB
Markdown
247 lines
6.4 KiB
Markdown
# 02. Agent Loop 执行机制
|
||
|
||

|
||
|
||
## 1. Agent Loop 的基本形态
|
||
|
||
Agent 每轮不是只调用一次模型,而是一个循环:
|
||
|
||
```text
|
||
用户输入
|
||
-> 构造 session 和 prompt
|
||
-> 调模型
|
||
-> 模型返回 assistant text 或 tool_calls
|
||
-> 如果没有 tool_calls:输出最终回复,结束
|
||
-> 如果有 tool_calls:执行工具
|
||
-> 工具结果写回 session
|
||
-> 下一轮模型继续读取工具结果
|
||
-> 直到最终回复、预算超限、取消、max_turns 或 review 停止
|
||
```
|
||
|
||
这个循环允许 Agent 做“观察-执行-再观察”的任务,例如:
|
||
|
||
- 先读文件,再决定是否需要抽取。
|
||
- 先生成 plan,等待用户 review。
|
||
- 先运行脚本,再根据校验结果修复。
|
||
- 先查询线上数据,再抽样展示。
|
||
|
||
## 2. 新会话和恢复会话
|
||
|
||
新会话入口:
|
||
|
||
```text
|
||
src/agent_runtime.py:413 run(...)
|
||
```
|
||
|
||
关键动作:
|
||
|
||
```text
|
||
1. 清理当前 managed_agent_id 和 resume_source_session_id。
|
||
2. 创建 session_id。
|
||
3. 创建 scratchpad_directory。
|
||
4. 绑定 plan_runtime/task_runtime 到 scratchpad。
|
||
5. 调 _run_prompt(...)
|
||
6. 累计 usage。
|
||
```
|
||
|
||
恢复会话入口:
|
||
|
||
```text
|
||
src/agent_runtime.py:441 resume(...)
|
||
```
|
||
|
||
关键动作:
|
||
|
||
```text
|
||
1. 从 StoredAgentSession 恢复 AgentSessionState。
|
||
2. 回放 file_history 和 compaction 信息。
|
||
3. 设置 active_session_id 和 last_session_path。
|
||
4. 恢复 plugin state。
|
||
5. 复用已有 scratchpad_directory。
|
||
6. 调 _run_prompt(...)
|
||
```
|
||
|
||
这解释了为什么刷新页面、回到旧会话后,Agent 理论上可以继续同一个 session 的上下文,而不是新建一个任务。
|
||
|
||
## 3. `_run_prompt` 主链路
|
||
|
||
核心位置:
|
||
|
||
```text
|
||
src/agent_runtime.py:520 _run_prompt 主体
|
||
```
|
||
|
||
主链路关键步骤:
|
||
|
||
```text
|
||
1. slash command 预处理。
|
||
2. hook policy / plugin hook 修改 prompt。
|
||
3. agent_manager.start_agent(...) 记录运行。
|
||
4. 新建或复用 AgentSessionState。
|
||
5. runtime_context prepend 到模型可见的用户消息。
|
||
6. session.append_user(...)。
|
||
7. tool_context 注入 scratchpad、plan_runtime、task_runtime。
|
||
8. 生成 tool_specs。
|
||
9. 初始化 usage、cost、tool_calls、events。
|
||
10. 进入 turn loop。
|
||
```
|
||
|
||
对应代码点:
|
||
|
||
```text
|
||
src/agent_runtime.py:523 agent_manager.start_agent(...)
|
||
src/agent_runtime.py:531 session = base_session or build_session(...)
|
||
src/agent_runtime.py:539 _prepend_runtime_context(...)
|
||
src/agent_runtime.py:543 session.append_user(...)
|
||
src/agent_runtime.py:546 replace(self.tool_context, scratchpad_directory=...)
|
||
src/agent_runtime.py:552 tool_specs = [tool.to_openai_tool() ...]
|
||
src/agent_runtime.py:582 stream_events = _RuntimeEventBuffer(event_sink)
|
||
src/agent_runtime.py:619 for turn_index in range(...)
|
||
```
|
||
|
||
## 4. 模型调用和 tool_calls 判断
|
||
|
||
模型调用入口:
|
||
|
||
```text
|
||
src/agent_runtime.py:1490 _query_model(...)
|
||
```
|
||
|
||
非流式路径中:
|
||
|
||
```text
|
||
turn = self.client.complete(
|
||
session.to_openai_messages(),
|
||
tool_specs,
|
||
output_schema=...
|
||
)
|
||
```
|
||
|
||
模型能否返回 `tool_calls` 取决于:
|
||
|
||
- 当前 `messages`。
|
||
- 系统提示词。
|
||
- Skill 列表。
|
||
- 工具描述和参数 schema。
|
||
- 模型自身 tool calling 能力。
|
||
|
||
基座不会强制某个工具被调用。基座只把工具能力暴露给模型,并在模型返回 `tool_calls` 后负责执行。
|
||
|
||
## 5. 没有 tool_calls 时
|
||
|
||
如果模型本轮没有工具调用:
|
||
|
||
```text
|
||
src/agent_runtime.py:763 if not turn.tool_calls
|
||
```
|
||
|
||
后续可能有三种情况:
|
||
|
||
1. 直接输出最终回复。
|
||
2. 如果模型输出被截断,自动追加 continuation prompt。
|
||
3. 如果达到 continuation 限制,则追加提示并结束。
|
||
|
||
最终会:
|
||
|
||
```text
|
||
session.append_assistant(...)
|
||
_append_final_text_stream_events(...)
|
||
AgentRunResult(...)
|
||
_persist_session(...)
|
||
```
|
||
|
||
## 6. 有 tool_calls 时
|
||
|
||
如果模型返回工具调用:
|
||
|
||
```text
|
||
src/agent_runtime.py:1008 for tool_call in turn.tool_calls
|
||
```
|
||
|
||
每个工具调用会:
|
||
|
||
```text
|
||
1. tool_calls 计数 +1。
|
||
2. 检查预算。
|
||
3. session.start_tool(...) 写入工具开始消息。
|
||
4. stream_events 追加 tool_start。
|
||
5. 根据工具名执行 handler。
|
||
6. 工具结果写回 session。
|
||
7. stream_events 追加工具结果。
|
||
8. 下一轮模型读取工具结果继续判断。
|
||
```
|
||
|
||
关键代码点:
|
||
|
||
```text
|
||
src/agent_runtime.py:1054 session.start_tool(...)
|
||
src/agent_runtime.py:1060 stream_events.append(type='tool_start')
|
||
src/agent_tools.py:60 execute_tool(...)
|
||
src/agent_tools.py:76 execute_tool_streaming(...)
|
||
```
|
||
|
||
## 7. 为什么 review 可以暂停流程
|
||
|
||
review 门禁不是特殊 UI 魔法,而是 Agent loop 的自然结果:
|
||
|
||
1. Skill 要求模型在某一步调用 review 工具。
|
||
2. 工具创建 pending state,并返回需要展示的信息。
|
||
3. Agent 生成回复,告诉用户需要确认。
|
||
4. 当前 run 结束。
|
||
5. 用户下一轮回复“确认”。
|
||
6. Agent resume 旧 session,调用 confirm 工具。
|
||
7. 后续流程继续。
|
||
|
||
例如 `product-data`:
|
||
|
||
```text
|
||
data_agent_prepare_generation_goal
|
||
-> pending goal
|
||
-> 停止等待用户确认
|
||
|
||
data_agent_confirm_generation_goal
|
||
-> confirmed_goal_id
|
||
-> data_agent_prepare_generation_plan
|
||
-> pending plan
|
||
-> 停止等待用户确认
|
||
|
||
data_agent_confirm_generation_plan
|
||
-> confirmed_plan_id
|
||
-> 允许生成 draft 和转换 records
|
||
```
|
||
|
||
## 8. 预算、取消和 max_turns
|
||
|
||
基座会在模型调用前后、工具请求前检查预算:
|
||
|
||
```text
|
||
src/agent_runtime.py:592 initial_budget = self._check_budget(...)
|
||
src/agent_runtime.py:732 budget_after_model = self._check_budget(...)
|
||
src/agent_runtime.py:1014 budget_after_tool_request = self._check_budget(...)
|
||
```
|
||
|
||
如果达到最大轮次:
|
||
|
||
```text
|
||
src/agent_runtime.py:1440 _build_max_turns_output(...)
|
||
```
|
||
|
||
输出会包含最后一次阶段说明,提醒用户可以继续补充指令。
|
||
|
||
取消由后端 run manager 和 tool process registry 处理。Web 点停止后,后端将 run 标记取消,并让工具执行上下文感知 cancel_event。
|
||
|
||
## 9. 事件流和前端活动区
|
||
|
||
Agent loop 内会不断追加 `stream_events`,后端用 `event_sink` 把事件推给前端。
|
||
|
||
后端关键代码:
|
||
|
||
```text
|
||
backend/api/server.py:1751 emit_agent_event(event)
|
||
backend/api/server.py:1756 state.run_manager.record_event(...)
|
||
backend/api/server.py:1758 state.run_state_store.record_event(...)
|
||
backend/api/server.py:2034 StreamingResponse(..., media_type='application/x-ndjson')
|
||
```
|
||
|
||
前端活动区展示的阶段说明、工具开始、工具完成、最终文本,本质上都来自这些 runtime events 或 session replay。
|