Agent 与 Runtime
Agent LoopRuntimeReActCoding Agent 和普通 LLM Chat 最核心的区别,不是 Prompt 更复杂,而是:
模型的输出可以变成真实动作,动作产生的新结果又会重新进入模型上下文。
于是一次请求不再是简单的:
User → LLM → Response而变成一个持续运行的闭环:
MyCode 的 AgentRunner 就是这个循环的核心 Runtime。
这一章先从最小 Agent Loop 开始,再逐步拆解:
- Agent 到底是什么;
- ReAct 与 Tool Calling 的关系;
- Agent Loop 如何运行;
- Runtime 应该控制什么;
- Tool Batch 如何执行;
- Progress / Stagnation 如何观察;
- Context、Permission、Session 等能力如何进入 Agent;
- MyCode 为什么没有把所有决策都写死在 Runtime 中。
1. 从 LLM 到 Agent
普通 LLM 调用的基本形式是:
模型能“告诉你怎么做”,但它本身没有真正操作项目。
Coding Agent 则需要把一部分环境能力提供给模型:
read_file
glob
grep
write_file
edit_file
run_command
...此时模型的响应可能不再是自然语言答案,而是:
read_file("src/parser.py")Runtime 执行工具以后得到:
ToolResult:
...然后再把这个结果提供给模型。
模型继续判断:
读取 tests/test_parser.py接着可能:
edit_file(...)最后:
run_command(["pytest", "tests/test_parser.py", "-q"])因此 Agent 最基本的结构其实可以概括为:
这就是一个最小的 Agent Loop(Agent 循环)。
2. ReAct:Reasoning + Acting
理解 Coding Agent 时经常会遇到 ReAct。
ReAct 可以简单理解为:
Reasoning
↓
Action
↓
Observation
↓
Reasoning
↓
...即模型不是一次性生成完整方案,而是:
- 根据当前信息判断下一步;
- 执行动作;
- 获取新的环境信息;
- 根据真实结果重新判断。
例如:
用户:
修复当前失败的测试。
Agent:
需要先看看有哪些测试失败。
Action:
run_command(["pytest", "-q"])
Observation:
2 failed, 18 passed
Agent:
失败集中在 parser,先查看对应测试。
Action:
read_file("tests/test_parser.py")
Observation:
...
Agent:
再读取实现。
Action:
read_file("src/parser.py")
Observation:
...
Agent:
发现 empty input 分支错误,修改代码。
Action:
edit_file(...)
Observation:
修改成功
Agent:
重新运行相关测试。
Action:
run_command(["pytest", "tests/test_parser.py", "-q"])
Observation:
8 passed
Agent:
任务完成。真正重要的是:
Agent 不需要在第一步就知道整个任务应该怎样完成。
它可以通过与环境交互逐步获得信息。
这也是 Coding Agent 比固定 Workflow 更适合处理开放式代码任务的原因之一。
3. Workflow 和 Agent 的区别
固定 Workflow 更像:
流程是开发者提前规定好的。
但真实 Coding Task 很难固定:
任务 A:
搜索 → 读文件 → 修改 → 测试
任务 B:
运行测试 → 搜错误 → 读配置 → 修改两个文件 → 再测试
任务 C:
读代码 → 调查依赖 → 委派 SubAgent → 修改 → lint → test
任务 D:
读文件 → 发现不需要修改 → 直接解释所以 MyCode 并没有把 Agent 写成:
read()
analyze()
edit()
test()
finish()而是让模型动态决定下一步 Tool Call。
Runtime 管理的是循环本身,而不是具体业务步骤。
4. MyCode 的核心 Agent Loop
MyCode 当前主循环位于:
mycode/agent/runner.py核心对象:
AgentRunner对外的主要执行入口是:
AgentRunner.run(user_message)它返回的是一个 Iterator[AgentEvent]。
也就是说,Agent 运行过程中并不是最后一次性返回所有结果,而是持续产生事件:
turn
model_start
text_delta
tool_call
tool_result
progress
...
stop因此 CLI、TUI、JSONL Runtime 等不同 Presentation Layer 都可以消费同一套 Agent 事件。
整体流程可以概括为:
这就是 MyCode Agent Runtime 最核心的骨架。
5. 一轮 Turn 发生了什么
一次 Agent Run 可以包含很多个 Turn。
假设:
max_turns = 50Runtime 会不断:
for turn_index in range(self.max_turns):每一轮主要经历以下阶段。
5.1 构建当前 Context
Runtime 先准备:
System Prompt
Conversation
Memory
Tool Results
Compact Summary
Runtime Guidance
Tool Schemas然后根据 Context Budget 构造真正发送给模型的消息。
这里需要区分:
Conversation History
≠
Current Model Context历史记录可能很多,但这一轮真正能发送给模型的信息受到 Context Window 限制。
Context 的具体处理会在 Context / Memory / Session 单独讨论。
5.2 把 Tool Schema 提供给模型
Runtime 从:
ToolRegistry获取当前可用工具 Schema。
因此模型当前能调用什么,不是单纯写死在 System Prompt 中,而取决于 Registry 中实际注册了什么 Tool。
例如基础 Agent 可能具有:
read_file
glob
grep
write_file
edit_file
run_command另外还可能动态加入:
Memory Tools
Skill Tools
read_artifact
delegate_task
MCP Tools之后 Runtime 调用模型:
LLM + Messages + Tool Schemas5.3 模型返回两类主要结果
从 Agent Runtime 的角度,模型这一轮最重要的结果只有两种。
返回 Tool Call
例如:
read_file(path="src/parser.py")说明:
任务还没有结束,需要继续和环境交互。
Runtime 执行工具,再进入下一轮。
返回普通文本且没有 Tool Call
例如:
已经修复 parser 的 empty input 问题,并通过相关测试。当前 MyCode 会把它视为:
final_answer然后结束本次 Agent Run。
因此:
6. Tool Call 为什么必须经过 Runtime
如果让模型直接执行任意程序,会产生大量问题。
因此:
LLM
↓
Tool Call
↓
Runtime
↓
Tool而不是:
LLM
↓
Operating SystemRuntime 可以在中间执行:
Schema Validation
Permission Check
Workspace Boundary
Risk Classification
Concurrency Scheduling
ToolResult Normalization这也是 Tool System 独立存在的原因。
后面的 Tool 系统 会详细拆这一层。
7. 一次响应可以调用多个 Tool
模型不一定一次只产生一个 Tool Call。
例如为了调查项目:
read_file("src/a.py")
read_file("src/b.py")
read_file("tests/test_a.py")如果这些工具彼此没有副作用,完全串行执行会浪费时间。
因此 MyCode 的 Tool Batch 支持:
Concurrency Safe Tool
↓
并行执行
Non-Concurrency-Safe Tool
↓
串行执行当前每组并发安全 Tool 最多同时执行:
4而模型单次响应中的 Tool Call 总数量也存在上限。
结构上可以理解为:
为什么不能所有工具都直接并发?
因为:
read A
read B
grep C通常互不影响。
但:
edit_file A
edit_file A
run_command则可能存在顺序和副作用问题。
所以 Tool 本身需要声明自己的并发安全语义,由 Runtime 负责调度。
8. ToolResult 如何重新进入循环
执行 Tool 以后,会生成统一的:
ToolResult例如:
ok
content
error
metadataRuntime 把结果写入 Conversation,使下一轮模型能够观察真实执行结果。
于是形成完整闭环:
这一步非常关键。
如果 ToolResult 不重新回填给模型,那么模型只是“发出了命令”,并不知道命令到底发生了什么。
9. Runtime 到底应该控制什么
Coding Agent 设计里很容易走向两个极端。
极端一:Runtime 什么都不管
模型想做什么就做什么。问题是容易出现:
- 无限循环;
- 重复读同一个文件;
- 重复执行同一个命令;
- Context 爆炸;
- 危险命令直接执行;
- Tool 协议错误;
- 长时间无法收敛。
极端二:Runtime 管得太多
例如 Runtime 强制规定:
必须先调查 8 次
必须再修改
必须验证 2 次
必须进入某个 Phase
必须按照固定顺序执行这样又会逐渐退化成 Workflow。
模型无法根据任务本身灵活决策。
MyCode 更希望维持这样的边界:
也就是:
Runtime 管约束和资源,不替模型完成任务规划。
10. Progress:Runtime 如何观察 Agent
Runtime 如果完全不理解工具执行发生了什么,就无法判断 Agent 是否陷入异常行为。
但 Runtime 同样不应该试图“理解代码语义”。
因此 MyCode 使用的是相对轻量的 Runtime Observation。
一次 Tool 执行后,可以观察:
tool_result
tool_signature
resource
mutation
validation
result_signature例如:
edit_file("src/a.py")可能被识别为:
mutation = yes
resource = file:src/a.py而:
pytest tests/test_a.py如果能够识别为验证命令并获得退出码,则可以形成:
validation = pass或:
validation = fail这里的重点是:
Runtime 记录的是事实信号,而不是直接替模型判断“任务已经完成”。
11. 为什么区分 Observation 和 Policy
这是 Runtime 设计里一个非常重要的思想。
例如 Runtime 观察到:
同一个 Tool 重复调用
同一个 Result 重复出现
同一个 Resource 被反复调查这是:
Observation但接下来怎么办属于:
Policy这两件事不应该混在一起。
MyCode 当前的结构是:
这样可以避免:
检测逻辑
=
控制逻辑紧紧耦合在一起。
12. Stagnation:Agent 为什么会重复调查
Coding Agent 很常见的一种失败不是直接报错,而是:
read_file A
grep B
read_file A
grep B
read_file A
...模型实际上没有获得新的有效信息,但仍持续消耗:
Turn
Token
Time
Tool CallMyCode 当前会观察三个维度:
same_tool_repeat
same_result_repeat
resource_repeat即:
Tool 是否重复
例如连续:
read_file(path="a.py")
read_file(path="a.py")ToolResult 是否重复
工具参数可能稍有变化,但实际返回信息没有变化。
Resource 是否重复
例如不同形式的操作最终都集中在同一个文件范围。
如果重复模式持续形成停滞,Runtime 会产生:
CONVERGENCE_GUIDANCE提醒模型:
当前出现重复主导的停滞特征,请重新评估当前假设和路径。
注意这里 Runtime 没有告诉模型下一步必须调用什么工具。
这是有意的。
Runtime 负责:
发现“你似乎卡住了”而不是:
直接命令“现在必须编辑这个文件”13. 重复 Tool Call 的硬边界
除了软性的 Stagnation Guidance,Runtime 还存在更基础的保护:
Repeated Tool Call Limit如果模型持续生成相同的 Tool Call,可以触发停止保护。
这和 Stagnation Policy 的作用并不完全一样:
Stagnation
→ 行为层面的软提示
→ 让模型重新评估
Repeated Tool Call Limit
→ Loop Safety Boundary
→ 防止明显的死循环因此 Runtime 中可以同时存在:
Soft Intervention
Hard Boundary两类机制。
14. Validation 在当前 Runtime 中是什么
MyCode 能识别一部分 Validation Command。
例如:
pytestRuntime 可以根据:
command
exit_code形成:
validation = pass或者:
validation = fail但这里需要明确当前实现的边界:
Validation 当前首先是一种 Runtime Observation,而不是一个通用的最终完成硬门槛。
也就是说 Runtime 能知道:
发生过修改
发生过测试
测试成功 / 失败 / 未知但当前公开实现并不会简单规定:
if mutation:
必须 validation == pass 才允许 final answer这是一个有意值得继续讨论的 Agent Runtime 设计问题:
Runtime 到底应该多大程度干预模型的完成判断?
过弱可能导致:
修改以后没有验证就结束。过强则可能导致:
非代码修改任务也被强制测试;
无法识别的自定义验证被错误阻止;
Runtime 逐渐接管模型决策。这也是 MyCode 演进过程中一直需要权衡的问题之一。
15. Context 也是 Runtime 的职责
Agent Loop 运行越久:
Conversation
ToolResult
Command Output
File Content
Memory都会不断增加。
所以每次真正调用模型之前,Runtime 都会构建:
ModelContext并检查 Context Budget。
如果已经超过模型可用范围,Runtime 不会继续盲目调用模型,而会以:
context_overflow停止。
Context 管理本身又涉及:
Tool Result Retention
Artifact
Compact
Memory Recall
Token Budget这一部分在 Context / Memory / Session 展开。
16. 为什么要有 Max Turns
Agent 理论上可以一直循环:
Model
→ Tool
→ Model
→ Tool
→ ...因此必须有明确的资源边界。
MyCode 当前主 Agent 默认存在最大 Turn 限制。
距离上限只剩少量 Turn 时,Runtime 会发送一次 Near-limit Guidance:
剩余轮次已经不多,
优先完成最重要的必要步骤。如果最终达到上限,Runtime 不会无限继续执行工具,而进入最终整理阶段。
如果任务仍未完成,可以生成用于后续继续执行的:
续跑检查点其中记录:
已确认事实
已读取文件及范围
已修改文件
测试状态
剩余动作
当前阻塞于是:
达到 Max Turns
↓
生成 Checkpoint
↓
结束当前 Run
↓
用户继续
↓
从 Checkpoint 衔接而不是下一轮完全从头重新调查。
17. 错误和 Retry
Agent Runtime 面对的不只是任务逻辑错误,还存在 Provider 层问题:
timeout
rate limit
connection error
stream failure
empty response因此 MyCode 在模型调用边界还存在 Retry。
部分可重试错误会:
Model Request
↓
Retryable Error
↓
Backoff
↓
Retry但 Retry 次数仍然有界。
另外,如果模型返回:
没有 Tool Call
+
没有有效文本Runtime 会额外提示一次模型继续任务或给出 Final Answer。
连续得到 Empty Response 后则终止为 Model Error。
这些都属于 Runtime 的职责:
模型可以做任务决策,但模型调用协议本身必须由程序保证可靠性。
18. AgentRunner 不是整个 MyCode
虽然 AgentRunner 是 Agent Loop 的核心,但完整运行环境还需要 Application Layer。
MyCode 启动一个真实 Agent Session 时,会进一步装配:
因此可以把 MyCode 分成两层来看。
Agent Runtime
负责:
Model ↔ Tool ↔ Result这个持续循环。
Application Runtime
负责:
Session
Persistence
MCP lifecycle
Workspace
Agent assembly以及一次 Agent 运行所需要的其他外围能力。
这种拆分也使 CLI、TUI 和 JSONL Runtime 不需要各自重新实现 Agent。
19. 一个 Coding Task 的完整路径
把目前讨论的内容放到一起,一个典型任务可以表示为:
这就是 MyCode 最基础的 Coding Agent 工作方式。
后面的 Tool、Context、SubAgent、MCP 等模块,本质上都是在扩展或者保护这一条主循环。
20. 为什么 MyCode 选择“模型决策 + Runtime 约束”
MyCode 当前更倾向于:
模型负责智能决策
+
Runtime 提供确定性约束而不是试图用 Runtime 写出一个“更聪明的规则系统”。
原因是 Coding Task 的变化非常大。
Runtime 很适合确定:
这个路径能不能访问
这个 Tool 参数是否合法
Context 有没有超预算
是否出现明显死循环
某些 Tool 是否可以并发
任务最多运行多少轮但 Runtime 很难可靠决定:
现在应该先读哪个文件
当前假设是否正确
这个 Bug 应该怎样修
应该写什么代码
下一步最有信息价值的调查是什么后者正是模型存在的意义。
所以比较理想的边界是:
可以概括成一句话:
Agent 的能力主要来自模型,Agent 的可靠运行来自 Runtime。
21. 这一章对应的源码
如果希望从源码继续阅读,可以重点看:
mycode/
├─ agent/
│ ├─ runner.py # Agent Loop / Tool Batch / Model 调用
│ ├─ progress.py # Runtime Observation / Stagnation Policy
│ ├─ events.py # AgentEvent
│ └─ outcome.py # Agent Run Outcome
│
├─ application/
│ ├─ runtime.py # AgentRunner 装配
│ └─ agent_session.py# Session + MCP + Runner 生命周期
│
├─ tools/
│ ├─ registry.py
│ └─ ...
│
└─ context/
└─ ...其中最核心的阅读顺序建议是:
AgentRunner.run()
↓
execute_tool_batch()
↓
RuntimeObservation
↓
decide_runtime_policy()
↓
build_agent_runner()
↓
AgentApplicationSession下一章
到这里,Agent Loop 本身已经建立起来。
但还有一个关键问题没有展开:
模型说
read_file、edit_file、run_command时,程序究竟如何把一个 LLM Tool Call 变成安全、可验证的真实执行?
这就是下一章的内容。
→ Tool 系统