Files
zk-data-agent/docs/technical-architecture/02-agent-loop.md
2026-05-18 19:28:02 +08:00

247 lines
6.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 02. Agent Loop 执行机制
![Agent Loop 执行机制](assets/02-agent-loop.png)
## 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。