第 9 章:Agent Loop——Ollama 如何让模型调用工具¶
一、Agent 不在 HTTP handler 里¶
Ollama 的 agent/ 是一层相对独立的运行时:它依赖 api.ChatRequest/Response 和一个 ChatClient,但不需要知道 Gin、manifest 或 GPU 调度细节。
flowchart TB
UI[cmd agent TUI / 外部宿主]
SES[agent.Session]
CHAT[ChatClient]
MODEL[/api/chat]
TOOLS[agent.Registry]
COMPACT[Compactor]
EVENTS[EventSink]
UI --> SES
SES --> CHAT
CHAT --> MODEL
SES --> TOOLS
SES --> COMPACT
SES --> EVENTS
这体现了一个清晰边界:server 提供模型能力,agent 决定怎样围绕模型组织多轮行动。
二、Session.Run 是一个有限状态机¶
agent/session.go 中的 Run 可以抽象成:
初始化 runState
→ 构造 system + 历史 + 当前输入
→ model step:Chat 流式接收
→ 如果没有 tool call:完成
→ tool step:审批、批量执行、写入 tool message
→ compaction step:必要时压缩
→ 回到 model step
stateDiagram-v2
[*] --> ModelStep
ModelStep --> Finish: stop / error / cancel
ModelStep --> ToolStep: assistant has tool calls
ToolStep --> Finish: denied / canceled / round limit
ToolStep --> CompactionStep: output exceeds budget
ToolStep --> ModelStep: append tool results
CompactionStep --> Finish: compaction failed/too large
CompactionStep --> ModelStep: compacted messages
runPhase、runFinish 和 toolExecutionStop 等类型的价值在于:取消、拒绝、错误和正常结束不会靠多个布尔值的组合猜测。
三、工具接口的最小面¶
agent.Registry 注册实现 Tool 接口的对象。工具通常需要:
Name():给模型和日志使用。Description():给模型的 tool schema 使用。Schema():参数 JSON schema。Execute():实际副作用。
可选接口用于安全控制:
ApprovalRequired:根据参数判断是否需要批准。ScopedTool:将批准范围细化到命令/路径/参数。
Tool
├── schema → 发给模型
├── requires approval? → 用户/宿主确认
├── approval scope → 缓存“允许什么”
└── Execute(ctx, ToolContext, args)
ToolContext 承载 working directory、session 信息等运行上下文,让工具实现不必依赖 Session 私有字段。
四、为什么工具调用是批量执行¶
一次 assistant response 可能包含多个 tool call。Session 会先解析完整 batch,再处理审批,然后执行 batch。这样做有三个好处:
- 用户能看到完整的即将执行动作,而不是每个工具调用一个不可预测的 prompt。
- “允许全部工具”或“允许这个工具/范围”可以在同一批次内复用。
- 服务器返回的 tool call 与 tool result 顺序能保持配对。
测试 TestSessionRunsFullApprovedToolBatchBeforeNextModelStep 直接表达了这个不变量:批准一批后,要执行完整批次,再进入下一次模型调用。
五、工具结果是上下文预算的第一受害者¶
工具可能返回大文件、日志或搜索结果。Session 在把结果写进历史前会通过 toolMessageWithBudget、Truncate 等函数按预算截断;如果上下文很小,还会使用更低的 preview cap。
截断通常保留头尾,并附带 marker,告诉模型中间内容被省略:
结果前部
...[tool output truncated: omitted N runes]...
结果尾部
这比直接丢弃结果更可解释,也比把全量结果塞进下一次请求更稳定。
六、Skills 是渐进式能力注入¶
agent/skills.go 会从默认目录和项目目录发现 skill,建立 SkillCatalog。系统上下文先列出技能目录/名称,真正激活时再读取 SKILL.md 和资源。
发现目录
→ 校验 skill name/front matter
→ catalog
→ system context 列出可用技能
→ 模型调用 activate skill
→ 追加 skill 内容/资源
这与一次性把所有技能全文塞进 system prompt 不同,属于渐进式披露:技能多时仍能控制初始上下文成本。
七、审批不是一个 yes/no¶
审批状态包含至少三种粒度:
本次拒绝
本批次允许
允许同工具/同 scope 的未来调用
ApprovalScope(args) 使 shell 命令、工作目录等危险参数可以细粒度缓存。测试还验证了批准后 working directory 会被冻结,避免模型在同一 batch 中先申请安全目录、再切换到另一个目录绕过批准。
八、设计取舍¶
- Agent 独立于 server:可在 TUI、外部宿主或测试 fake client 中运行。
- 工具由接口提供:核心只编排,不内置所有副作用。
- 审批与 tool 解耦:安全策略可按工具和参数实现。
- Skills 延迟加载:能力多时节省上下文,但需要发现、校验和诊断机制。
- 事件显式发出:UI 不必窥探 Session 私有状态。