跳转至

第 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

runPhaserunFinishtoolExecutionStop 等类型的价值在于:取消、拒绝、错误和正常结束不会靠多个布尔值的组合猜测。

三、工具接口的最小面

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。这样做有三个好处:

  1. 用户能看到完整的即将执行动作,而不是每个工具调用一个不可预测的 prompt。
  2. “允许全部工具”或“允许这个工具/范围”可以在同一批次内复用。
  3. 服务器返回的 tool call 与 tool result 顺序能保持配对。

测试 TestSessionRunsFullApprovedToolBatchBeforeNextModelStep 直接表达了这个不变量:批准一批后,要执行完整批次,再进入下一次模型调用。

五、工具结果是上下文预算的第一受害者

工具可能返回大文件、日志或搜索结果。Session 在把结果写进历史前会通过 toolMessageWithBudgetTruncate 等函数按预算截断;如果上下文很小,还会使用更低的 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 私有状态。