跳转至

Agent API 参考

English · 简体中文

本文档描述 P-A-R SDK 的 Agent 配置、运行时管理和工具注册 API。

运行时配置

runtime_config

运行时通过 Par.Runtime.create 创建,需要以下配置:

type runtime_config = {
  persistence : [ `Sqlite of string ];
  event_bus : event_bus_config;
  default_quota : resource_quota;
  shutdown : shutdown_config;
  llm_providers : (string * llm_provider_config) list;
  eval_limits : eval_limits;
  parallel_tool_execution : bool;
  bash_confirm : bash_confirm_config;
  event_retention_seconds : float;
}

Par.Runtime 提供以下默认配置值,可以直接使用:

Runtime.default_event_bus_config   (* buffer_capacity=10000, DLQ 开启 *)
Runtime.default_quota             (* max_concurrent_tasks=10 *)
Runtime.default_shutdown_config   (* drain_timeout=30s *)
Runtime.default_bash_confirm      (* Always 策略 *)

创建运行时

val Runtime.create :
  ?persistence:persistence_service ->
  ?event_bus:Types.event_bus_service ->
  ?llm:llm_service ->
  ?embeddings:embedding_service ->
  ?memory:memory_service ->
  ?vector_store_backend:Types.vector_store_backend ->
  ?bash_policy:(module Bash_policy.POLICY) ->
  ?workspace:Workspace.workspace ->
  ?mcp_servers:Mcp_types.server_config list ->
  ?mcp_process_mgr:_ Eio.Process.mgr ->
  ?mcp_net:_ Eio.Net.t ->
  ?mcp_clock:_ Eio.Time.clock ->
  ?mcp_startup_policy:Mcp_types.startup_policy ->
  ?net:_ Eio.Net.t ->
  config:runtime_config ->
  Eio.Switch.t ->
  (runtime, error_category) result

所有可选参数默认为 None。关键可选参数:

参数 说明
?persistence 持久化后端(如 SqliteNoop)。
?event_bus 自定义事件总线配置。
?llm 主 LLM 服务 provider。
?embeddings 嵌入服务,用于 RAG 管道。见 RAG API
?memory 内存服务,用于跨会话 agent 记忆(FTS5)。见 Memory API
?vector_store_backend 向量存储后端,用于 RAG 相似度搜索。见 RAG API
?bash_policy Bash 信任边界策略模块。默认:Always(允许所有)。
?workspace 文件系统沙箱的 Workspace。默认为 CWD。
?mcp_servers 创建时启动的 MCP 服务器配置。
?mcp_process_mgr MCP stdio 服务器的 Eio 进程管理器。
?mcp_net MCP HTTP/SSE 服务器的 Eio 网络能力。
?mcp_clock MCP 启动超时的 Eio 时钟。
?mcp_startup_policy MCP 服务器启动策略(阻塞 vs 延迟)。
?net Runtime 全局出站 I/O 的 Eio 网络能力(与 ?mcp_net 分开,后者仅作用于 MCP 服务器)。

完整示例:

open Par

let config = {
  persistence = `Sqlite "par.db";
  event_bus = Runtime.default_event_bus_config;
  default_quota = Runtime.default_quota;
  shutdown = Runtime.default_shutdown_config;
  llm_providers = [];
  eval_limits = { max_depth = 10; max_node_visits = 1000 };
  parallel_tool_execution = true;
  bash_confirm = Runtime.default_bash_confirm;
  event_retention_seconds = 604800.0;
}

let () = Eio_main.run (fun _env ->
  Eio.Switch.run (fun switch ->
    match Runtime.create ~config switch with
    | Error _ -> Printf.eprintf "Runtime creation failed\n"
    | Ok rt ->
      (* ... 使用运行时 ... *)
      let exit_code = Runtime.close rt in
      exit exit_code
  )
)

Agent 配置

agent_config

type agent_config = {
  id : string;                            (* Agent 唯一标识 *)
  system_prompt : system_prompt;          (* 类型化记录:{ sp_raw : string; sp_zone : zone_tag } *)
  system_prompt_template : system_prompt_template option;  (* 可选模板化提示词(带变量)*)
  model : model_config;                   (* LLM 模型配置 *)
  tools : tool_descriptor list;           (* 可用工具列表 *)
  max_iterations : int;                   (* ReAct 循环最大迭代次数 *)
  middleware : middleware_hook list;       (* 中间件管道 *)
  retry_policy : retry_policy option;     (* 可选重试策略 *)
  context_strategy : context_strategy option;  (* 上下文窗口管理策略 *)
  resource_quota : resource_quota option;  (* 可选资源配额覆盖 *)
  max_execution_time : float option;      (* 可选最大执行时间(秒)*)
  early_stopping_method : early_stopping_method;  (* 达到迭代上限时:Force 或 Generate *)
  on_max_tokens : on_max_tokens_behavior option;  (* None=Auto(默认),或显式 Retry/Continue/Return_partial *)
  max_continuation_chunks : int option;           (* None=Auto(默认),或显式上限 *)
  tool_timeout : float option;            (* 可选单次工具调用超时(秒)*)
  context_compression_threshold : float option;   (* v0.6.3+:按比例自动压缩。None=手动模式,Some 0.8=默认 *)
  compression_cooldown_messages : int option;     (* v0.6.3+:两次自动压缩间最小迭代数。Some 6=默认 *)
  context_window_override : int option;           (* v0.6.3+:覆盖 context window 大小;None=用 provider capability 或静态表 *)
  cache_strategy : cache_strategy;        (* 提示词缓存策略:No_caching | With_cache_of of cache_ttl *)
  approval_handler : approval_context Approval.approval_handler option;
                                          (* v0.8.0+:可选 HITL 审批 handler。None=用 runtime 默认 handler。
                                             设为 Some _ 时,此 agent 的 ReAct 循环遇到 Approval_required
                                             工具结果会挂起,并通过 Runtime.resume_approval 恢复。 *)
}

自动上下文压缩(v0.6.3+)

context_compression_threshold 设置时(默认 Some 0.8),引擎在每次 LLM 调用前检查 估算 tokens / context window 比例。如果超过阈值且冷却已过,应用配置的 context_strategy (或 context_strategy = None 时默认用 Summarize)。

Context window 大小通过三层 resolver 解析: 1. context_window_override(用户 supplied,优先级最高) 2. llm_service.context_window_fn(provider capability 函数) 3. 静态查表(default_context_window):gpt-4o 系列=128K,claude-4 系列=200K,gpt-3.5-turbo=16385,未知=8000(保守默认)

两个可观测事件: - Context_compressed { trigger; tokens_before; tokens_after; messages_before; messages_after; strategy_used; elapsed_ms } 压缩成功时 - Context_compression_skipped { reason } 跳过时,带类型化 reason:`Below_threshold of float`Cooldown_active of int`No_window_size`No_strategy

默认值变更(0.x 中的 BREAKING)make_agent 默认 context_strategySliding_window { max_messages=100; max_tokens=200000 } 改为 Summarize { max_tokens=8000; summary_model=None }。 业界调研确认所有主流生产 agent 框架的默认值都是 LLM-summarize(Letta、Anthropic、LangChain、CrewAI), 零个用 truncate-drop。要恢复 v0.6.3 前的行为,显式设置 context_strategy = Some (Sliding_window {...})

model_config

type model_config = {
  provider : [ `Openai | `Anthropic | `Ollama | `Custom of string ];
  model_name : string;
  api_base : string option;          (* 自定义 API 端点 *)
  temperature : float;
  max_tokens : int option;
  top_p : float option;
  stop_sequences : string list option;
}

Provider 示例:

(* OpenAI *)
{ provider = `Openai; model_name = "gpt-4"; api_base = None;
  temperature = 0.7; max_tokens = Some 4096; top_p = None;
  stop_sequences = None }

(* 通过 ZhipuAI 中转调用 Anthropic *)
{ provider = `Anthropic; model_name = "claude-sonnet-4-20250514";
  api_base = Some "https://open.bigmodel.cn/api/paas/v4";
  temperature = 0.5; max_tokens = None; top_p = None;
  stop_sequences = None }

(* Ollama 本地模型 *)
{ provider = `Ollama; model_name = "llama3"; api_base = None;
  temperature = 0.8; max_tokens = None; top_p = None;
  stop_sequences = None }

(* 自定义端点 *)
{ provider = `Custom "my-provider"; model_name = "my-model";
  api_base = Some "http://localhost:8000/v1";
  temperature = 0.7; max_tokens = None; top_p = None;
  stop_sequences = None }

LLM Provider 配置

Provider 实例通过 llm_provider_config 创建,传递给 Runtime.createllm 参数:

type llm_provider_config =
  | Openai of { api_key : string; base_url : string option;
                organization : string option;
                embedding_model : string option;
                prompt_cache_key : string option }
  | Anthropic of { api_key : string; base_url : string option }
  | Ollama of { base_url : string }
  | Custom of { base_url : string; headers : (string * string) list;
                request_format : [ `Openai_compatible | `Anthropic_compatible ] }

运行时操作

注册 Agent

val Runtime.register_agent : runtime -> agent_config -> (unit, error_category) result
let agent = {
  Types.id = "my-agent";
  system_prompt = Types.stable_prompt "You are a helpful assistant.";
  model = { provider = `Openai; model_name = "gpt-4"; api_base = None;
            temperature = 0.7; max_tokens = None; top_p = None;
            stop_sequences = None };
  tools = [ tool.descriptor ];   (* 来自 Runtime.register_tool *)
  max_iterations = 5;
  middleware = [];
  retry_policy = None;
  context_strategy = None;
  resource_quota = None;
  max_execution_time = None;
  early_stopping_method = Types.Force;
  on_max_tokens = None;              (* Auto:Return_partial(此 agent 有工具)*)
  max_continuation_chunks = None;    (* Auto:3(有工具 agent 的默认值)*)
  tool_timeout = None;
} in
ignore (Runtime.register_agent rt agent)

调用 Agent

val Runtime.invoke :
  runtime ->
  agent_id:string ->
  message:string ->
  ?workspace:Workspace.workspace ->
  ?cancellation_token:cancellation_token ->
  ?conversation:conversation ->
  ?on_tool_event:(event -> unit) ->
  ?on_chunk:(llm_response_chunk -> unit) option ->
  ?enable_handoff:bool ->
  ?system_prompt_appendix:string ->
  ?skills:string list ->
  ?context:Invoke_context.invoke_context ->
  ?update_current:bool ->
  ?save:bool ->
  unit ->
  (invoke_result, error_category * conversation) result

所有可选参数:

参数 类型 说明
?workspace Workspace.workspace 单次调用 workspace 覆盖。工具使用此 workspace 而非运行时默认值。
?cancellation_token cancellation_token 协作取消令牌。见下方「取消令牌」章节。
?conversation conversation option 恢复的对话历史。传 None 开始新对话。
?on_tool_event event -> unit 工具相关事件回调(tool_call_sent、tool_result_received 等)。
?on_chunk (llm_response_chunk -> unit) option LLM 响应块的流式回调。None 禁用流式。
?enable_handoff bool 启用 agent 间交接(handoff)工具。默认:false
?system_prompt_appendix string 仅本次调用追加到 system prompt 的文本。见 invoke_context
?skills string list 单次调用激活 Skills 的覆盖(v0.8.1+)。传入 skill id 列表只激活这些 skill,忽略各 skill 的 trigger 模式。见 Skills API
?context Invoke_context.invoke_context 预构建的单次调用隔离上下文。提供时使用此上下文而非创建新的。见 invoke_context
?update_current bool true 时,将本次调用产生的对话回写到 agent 当前的 conversation handle(v0.7.7+)。默认遵循 runtime 的持久化策略。
?save bool true 时,将产生的对话持久化到持久化后端(v0.7.7+)。为 false 时,对话仅保留在内存中。默认遵循 runtime 的持久化策略。

返回类型为 invoke_result,它包裹 llm_response

type llm_response = {
  text : string option;
  reasoning_content : string option;
  tool_calls : tool_call list option;
  finish_reason : finish_reason;
  usage : usage_stats;
  model : string;
}

reasoning_content 仅由推理模型(OpenAI o1/o3、DeepSeek-R1 通过 OpenAI 兼容 API)填充。当 provider 以独立字段返回推理内容时,它持有模型的思维链文本。对于非推理模型,此字段为 None。设置时,模型的实际回答仍在 text(或 tool_calls)中,因此两个字段可以同时携带数据。

type invoke_result = {
  response : llm_response;
  conversation : conversation;
  approval_pending : approval_pending_info option;
}

当 agent 配置了 async/webhook approval_handler,且 ReAct 循环遇到 Approval_required 工具结果时,引擎会挂起循环并以 approval_pending = Some { run_id; agent_id; tool_name; expires_at } 返回(v0.8.0+)。调用方应将 run_id 传给 Runtime.resume_approval 来解析请求并继续执行。None 表示本次调用没有为审批而挂起。

错误元组中的 conversation 字段携带失败时的对话状态,支持错误恢复或部分结果提取。

match Runtime.invoke rt ~agent_id:"my-agent" ~message:"Hello!" () with
| Ok result ->
  let resp = result.response in
  (match resp.text with Some text -> Printf.printf "Response: %s\n" text
  | None -> Printf.printf "No text response\n")
| Error (err, _conv) ->
  Printf.eprintf "Error: %s\n"
    (Types.error_category_to_yojson err |> Yojson.Safe.to_string)

结构化输出

Runtime.invoke_structured 返回通过 schema 校验的 JSON,而非自由文本。函数签名:

val Runtime.invoke_structured :
  runtime ->
  agent_id:string ->
  message:string ->
  response_schema:Yojson.Safe.t ->
  ?max_repair_attempts:int ->
  ?cancellation_token:cancellation_token ->
  ?conversation:conversation ->
  ?system_prompt_appendix:string ->
  ?on_tool_event:(event -> unit) ->
  ?on_repair_attempt:(int -> error_category -> conversation -> unit) ->
  unit ->
  (structured_invoke_result, error_category * conversation) result

返回类型 structured_invoke_result 携带校验后的 JSON、原始 LLM 响应、完整对话历史和 repair-loop 重试次数:

type structured_invoke_result = {
  value : Yojson.Safe.t;          (* 通过 schema 校验的 JSON *)
  raw_response : llm_response;   (* 原始 LLM 响应(调试 / token 核算) *)
  conversation : conversation;   (* 包含所有 repair 轮的完整对话历史 *)
  attempts : int;                (* 1 = 一次性成功;>1 = repair-loop 触发 *)
}

Json_extract.extract_json_from_text 无法从 LLM 文本解析出有效 JSON,或解析后的 JSON 无法通过 Validation.validate_tool_input_result 的 schema 校验时,repair loop 会触发。最多重试 max_repair_attempts(默认 3)次,每次都会在对话中追加 user-feedback 消息。每次迭代顶部的 cancellation token 检查防止无限制的 LLM 调用。

工具执行 + 结构化输出(v0.7.4+):当 agent 注册了工具且你需要校验后的 JSON 时,runtime 会自动路由到 Engine.run_agent_structured —— 一种两阶段模式:

  1. 阶段一:完整 ReAct 循环运行 agent 与工具(bash、http、自定义)交互,直到 LLM 产出最终文本响应。
  2. 阶段二:完整对话历史(包括所有工具调用结果)传给一个独立的结构化 LLM 调用,提取并对照 response_schema 校验 JSON。

此模式对齐 LangGraph 的 create_react_agent(response_format=) 方案。它能跨所有 LLM provider(OpenAI、Anthropic、Ollama、自定义)工作,不依赖 provider 原生支持同时传 tools + response_format(在非 OpenAI provider 上不可靠 —— 见 CrewAI issue #5472)。

原生结构化输出(v0.7.5+):OpenAI 和 Anthropic provider 现在使用原生结构化输出模式,而非之前的文本注入回退。OpenAI 发送 response_format: {type: json_schema, json_schema: {name, schema, strict: true}} 以实现严格的 JSON schema 校验。Anthropic 发送 output_config: {format: {type: json_schema, schema}}。Ollama 和 Custom provider 继续使用文本注入回退(基于 prompt 的 JSON 提取 + schema 校验),因为它们可能不支持严格的 JSON schema 模式。

match Runtime.invoke_structured rt
    ~agent_id:"env-detector"
    ~message:"检查项目并报告运行时环境"
    ~response_schema:(`Assoc [
      ("language", `String "OCaml");
      ("ocaml_version", `String "5.x.x");
      ("dune_version", `String "3.x.x");
    ]) () with
| Ok { value; _ } -> Yojson.Safe.to_string value
| Error (err, _conv) -> Printf.eprintf "Error: %s" (Types.error_category_to_string err)

如果 agent 没有工具(config.tools = []),invoke_structured 直接走轻量级 run_structured 路径 —— 无 ReAct 循环,单次 LLM 调用。设置 ?on_tool_event 可在 ReAct 阶段观察工具调用。

流式示例

Runtime.invoke rt ~agent_id:"my-agent" ~message:"Tell me a story"
  ~on_chunk:(fun chunk ->
    Printf.printf "%s%!" chunk.text)
  ()

异步调用

Runtime.invoke_async 在后台 fiber 中运行调用,立即返回一个 invoke_handle,可用于等待、取消或轮询结果。完整详情见 invoke_context

val Runtime.invoke_async :
  runtime ->
  agent_id:string ->
  message:string ->
  ?workspace:Workspace.workspace ->
  ?cancellation_token:cancellation_token ->
  ?conversation:conversation ->
  ?on_tool_event:(event -> unit) ->
  ?on_chunk:(llm_response_chunk -> unit) option ->
  ?enable_handoff:bool ->
  ?system_prompt_appendix:string ->
  ?context:Invoke_context.invoke_context ->
  unit ->
  Invoke_context.invoke_handle

Handle 函数:

val Invoke_context.invoke_handle_await :
  invoke_handle ->
  (invoke_result, error_category * conversation) result

val Invoke_context.invoke_handle_cancel : invoke_handle -> unit
val Invoke_context.invoke_handle_status : invoke_handle -> invoke_status
let handle = Runtime.invoke_async rt ~agent_id:"researcher"
  ~message:"Find recent papers on OCaml effects" () in
(* 在 agent 后台运行时做其他事情 ... *)
match Invoke_context.invoke_handle_await handle with
| Ok result ->
  Printf.printf "Done: %s\n" (result.response.text |> Option.value ~default:"")
| Error (err, _) ->
  Printf.eprintf "Failed: %s\n"
    (Types.error_category_to_yojson err |> Yojson.Safe.to_string)

关闭运行时

val Runtime.close : runtime -> int   (* 返回退出码 *)

Runtime.close 也会停止所有通过 Runtime.mcp_server 启动的 MCP 服务器子进程;返回的整数是退出码,非零值表示有子进程拒绝干净退出。

Python dispatch-queue:invoke_start / invoke_poll / invoke_cancel

Python 绑定提供非阻塞的 dispatch-queue 三件套,用于从 Python 进行取消安全的 invoke。阻塞的 Runtime.invoke 在整个 ReAct 循环期间持有 OCaml 锁,这意味着 Python 回调在锁持有期间无法取消。dispatch-queue 三件套是从 Python 取消的唯一正确隔离路径。

API

# 启动调用——返回 handle id(字符串)
handle_id: str = rt.invoke_start(agent_id, message, *, save=None, update_current=None)

# 轮询结果——返回带 status: "pending" / "ok" / "error" / "cancelled" 的字典
result: dict = rt.invoke_poll(handle_id, timeout_ms=0)

# 取消调用
rt.invoke_cancel(handle_id) -> None

使用示例

from par_runtime import Runtime
import json

config = json.dumps({"persistence": {"tag": "sqlite", "contents": ":memory:"}})

with Runtime(config) as rt:
    # 启动长时间运行的调用
    handle = rt.invoke_start("my-agent", "Research OCaml 5 effects")

    # 轮询直到完成
    while True:
        result = rt.invoke_poll(handle, timeout_ms=100)
        status = result.get("status")
        if status == "ok":
            print(f"Done: {result['text']}")
            break
        elif status in ("error", "cancelled"):
            print(f"Failed: {result}")
            break
        # status == "pending"——做其他事,然后再次轮询

    # 或从另一个线程/fiber 取消
    # rt.invoke_cancel(handle)

终端轮询消耗 handle

返回终端状态(okerrorcancelled)的轮询会消耗 handle。终端轮询后,handle 不再有效,进一步调用 invoke_pollinvoke_cancel 会返回错误。这是设计如此:防止结果被重复消费。

何时使用三件套 vs 阻塞 invoke

模式 适用场景
rt.invoke(agent_id, message) 简单同步调用,不需要取消
rt.invoke_start / invoke_poll / invoke_cancel 需要从 Python 取消,或需要在 agent 运行时做其他事
Runtime.invoke_async(OCaml) OCaml 端异步,用 invoke_handle_await / invoke_handle_cancel

三件套和 invoke_async 解决相同的问题(带取消的非阻塞 invoke),分别在各自的语言表面上。三件套存在的原因是 Python 的 invoke 在整个循环期间持有 OCaml 锁,使得 Python 端取消在没有单独 dispatch 路径的情况下不可能实现。

工具注册

tool_descriptor

type tool_descriptor = {
  name : string;
  description : string;
  input_schema : Yojson.Safe.t;     (* JSON Schema 格式 *)
  output_schema : Yojson.Safe.t option;  (* 可选的工具输出 JSON Schema *)
  permission : tool_permission;     (* 默认 Allow *)
  timeout : float option;
  concurrency_limit : int option;
  on_update : (string -> unit) option;  (* 可选的进度回调 *)
  cache_control : cache_control option;  (* 可选的提示词缓存断点 *)
}

handler 函数签名

工具 handler 接收 JSON 输入和取消令牌,返回 handler_result

type handler_fn = Yojson.Safe.t -> Types.cancellation_token -> Types.handler_result

type handler_result =
  | Success of Yojson.Safe.t
  | Error of {
      category : error_category;
      message : string;
      retryable : bool;
      metadata : (string * Yojson.Safe.t) list;
    }
  | Handoff of {
      target_agent_id : string;
      carry_context : bool;
      task : string option;
    }

Runtime.register_tool

val Runtime.register_tool :
  runtime ->
  name:string ->
  description:string ->
  input_schema:Yojson.Safe.t ->
  handler:handler_fn ->
  ?output_schema:Yojson.Safe.t ->
  ?permission:tool_permission ->
  ?timeout:float ->
  ?concurrency_limit:int ->
  ?on_update:(string -> unit) option ->
  ?cache_control:cache_control option ->
  unit ->
  (tool_binding, error_category) result

tool_binding 包含 descriptor(用于 agent_config.tools)和 handler(已自动注册到 registry):

type tool_binding = {
  descriptor : tool_descriptor;
  handler : Yojson.Safe.t -> cancellation_token -> handler_result;
}

工具注册示例

(* 定义一个计算器工具 *)
let calc_tool = Runtime.register_tool rt
  ~name:"calculator"
  ~description:"Evaluate a math expression"
  ~input_schema:(`Assoc [
    ("type", `String "object");
    ("properties", `Assoc [
      ("expression", `Assoc [
        ("type", `String "string");
        ("description", `String "The math expression to evaluate");
      ])
    ]);
    ("required", `List [`String "expression"]);
  ])
  ~handler:(fun input token ->
    match input with
    | `Assoc fields ->
      (match List.assoc_opt "expression" fields with
       | Some (`String expr) ->
         (try
           let result = float_of_string expr in
           Types.Success (`Float result)
         with _ ->
           Types.Error {
             category = Types.Invalid_input "Invalid expression";
             message = "Could not parse expression";
             retryable = false;
             metadata = [];
           })
       | _ -> Types.Error {
           category = Types.Invalid_input "Missing expression";
           message = "Expression field is required";
           retryable = false;
           metadata = [];
         })
    | _ -> Types.Error {
        category = Types.Invalid_input "Invalid input";
        message = "Input must be a JSON object";
        retryable = false;
        metadata = [];
      })
  ()

ReAct 循环

Agent 执行核心是 Par.Engine.run_agent

  1. 构建对话(System prompt + User message)
  2. 应用 context_strategy(如已配置)管理上下文窗口
  3. 通过中间件链发送 on_before_llm 钩子
  4. 调用 LLM Provider 获取响应
  5. 通过中间件链发送 on_after_llm 钩子
  6. 若响应包含 tool_calls,依次执行每个工具:
  7. 查找工具 descriptor
  8. 从 Tool_registry 解析 handler
  9. 执行工具(经过 on_before_tool / on_after_tool 钩子)
  10. 将工具结果追加到对话
  11. 递归调用直到 finish_reasonStop 或达到 max_iterations

max_iterations 行为

当迭代次数达到 max_iterations 时,run_agent 返回: Result.Error (Internal "Max iterations exceeded")

on_max_tokens 行为

v0.6.x 行为变更:自 v0.6.x 起,on_max_tokens 改为 option 类型。None(新默认值)表示 Auto —— 运行时根据 effective tool 集解析策略:无工具 agent 自动选 Continue(长输出生成模式),有工具 agent 选 Return_partial(向后兼容默认值)。显式 Some Return_partial / Some Retry / Some Continue 总是覆盖 Auto。max_continuation_chunks 同样遵循 Auto 逻辑:None 表示无工具 agent 无上限,有工具 agent 上限为 3。

当 LLM 返回 finish_reason=Max_tokens(截断响应)时,行为取决于 agent.on_max_tokens

  • Return_partial(默认):若截断响应包含非空文本,保留并返回 Ok 及部分结果。空截断保留错误/重试行为。
  • Retry:保留截断消息作为上下文,重新进入 ReAct 循环(受 max_iterations 约束)。
  • Continue:注入"从上次中断处继续"的跟进消息,拼接续写块直到 finish_reason=Stop。受 max_continuation_chunks(默认 3)限制。递减收益保护:若续写块新增少于 500 字符则停止。

每次截断都会发出 Llm_response_truncated 事件用于可观测性。

System Prompt 设计建议

  • 明确指定 Agent 的角色和能力边界
  • 列出可用工具的用途和使用场景
  • 指定输出格式要求(JSON、纯文本等)
  • 对需要多步推理的任务,提示 Agent 逐步思考
system_prompt = {|
  You are a data analysis assistant. You have access to a calculator tool.
  When asked to compute something:
  1. Identify the mathematical expression
  2. Use the calculator tool
  3. Present the result clearly

  Always show your reasoning step by step.
|}

System Prompt 模板

当系统提示词需要在每次调用时变化(注入 agent id、运行时 id、可用工具列表或用户提供的变量),可以使用 system_prompt_template 代替普通 system_prompt。作为 agent_config.system_prompt_template 字段添加,它提供 mustache 风格的 {{variable}} 替换,无需引入额外的模板依赖。

模板类型

type system_prompt_template = {
  template : string;        (* 带 {{var}} 占位符的正文 *)
  variables : string list;  (* 模板可能使用的所有占位符名 *)
  required : string list;   (* 必须提供的 `variables` 子集 *)
}

variables 是渲染器识别的所有变量名集合。required 是渲染时必须存在的子集;缺少 required 变量会返回 Error 而非静默替换为空字符串。将两个列表分开让模板可以声明可选的辅助变量(用户的 locale、session 标签)和硬性要求(agent id)。

render_context

渲染器从 render_context 记录中提取值。运行时内部构建它;如果你手动调用 Template.render,则需要自己构造。

type render_context = {
  agent_id : string;
  runtime_id : string;
  user_variables : (string * Yojson.Safe.t) list;
  available_tools : string list;
}

agent_idruntime_id 始终可用。available_tools 是 agent 上注册的工具名列表。user_variables 是调用方提供的值。渲染器在解析 {{name}} 时查阅这四个字段。

渲染

val Template.render :
  template:string ->
  variables:(string * Yojson.Safe.t) list ->
  required:string list ->
  context:render_context ->
  (string, Types.error_category) result

val Template.effective_system_prompt :
  Types.agent_config ->
  runtime_id:string ->
  (string, Types.error_category) result

Template.render 是低级入口。effective_system_prompt 是便捷包装:传入 agent_configruntime_id,返回引擎将发送给 LLM 的最终字符串。如果 system_prompt_templateNone,则回退到普通 system_prompt 字段,所以现有 agent 无需改动即可继续工作。

何时使用模板

当以下任一条件满足时使用模板:

  • 提示词需要引用 agent id 或运行时 id,且你不想手动插值。
  • 提示词需要每次调用时的变量(用户 locale、session 元数据、动态示例),由调用方提供。
  • 你希望渲染器在启动时强制检查 required 变量,而非在对话中途才发现缺失值。

当文本是静态的时,使用普通 system_promptvariables 为空列表的模板没有收益,只会增加渲染步骤。

示例

let agent = {
  Types.id = "support";
  system_prompt = Types.stable_prompt "You are a helpful assistant.";   (* 回退值 *)
  system_prompt_template = Some {
    template = {|
      You are {{role}}, assisting agent {{agent_id}} on runtime {{runtime_id}}.
      Available tools: {{available_tools}}.
      User context: {{user_locale}}.
    |};
    variables = ["role"; "agent_id"; "runtime_id";
                 "available_tools"; "user_locale"];
    required = ["role"; "user_locale"];
  };
  model = (* ... *);
  tools = [];
  max_iterations = 5;
  middleware = [];
  retry_policy = None;
  context_strategy = None;
  resource_quota = None;
  max_execution_time = None;
  early_stopping_method = Types.Force;
  on_max_tokens = None;              (* Auto:Continue(此 agent 无工具)*)
  max_continuation_chunks = None;    (* Auto:无上限(无工具长输出模式)*)
  tool_timeout = None;
}

在调用时,渲染器从运行时替换 agent_idruntime_idavailable_tools,从每次调用的 user_variables 中提取 roleuser_locale。因为两者都是 required,忘记 user_locale 的调用方会在第一次 LLM 往返之前收到 Error,而不是一个乱码的提示词。

上下文策略

长时间对话会超出模型的上下文窗口。agent_config.context_strategy 字段决定 PAR 在每次 LLM 调用前如何裁剪对话。保持为 None 时运行时应用默认值;显式设置可覆盖。

策略变体

type context_strategy =
  | Truncate_oldest of { keep_system : bool; min_messages : int }
  | Summarize of { max_tokens : int; summary_model : model_config option }
  | Sliding_window of { max_messages : int; max_tokens : int }

Truncate_oldest 丢弃最旧的非系统消息直到对话适合。keep_system(默认 true)将系统消息固定在前面;min_messages 是下限,即使 token 估算超预算也不会丢弃更多消息。当近期对话轮次携带主要信号、旧轮次是噪声时使用此策略。

Summarize 使用第二次 LLM 调用将早期轮次压缩为摘要消息。max_tokens 限定摘要长度。summary_model 可选地将摘要调用路由到比 agent 主模型更便宜或更快的模型;None 复用 agent 自身的 model_config。当早期上下文确实重要但 token 预算紧张时选择此策略,代价是每次摘要多一次 LLM 往返。

Sliding_window 保留最近的 max_messages 条消息,丢弃所有更旧的内容,受 max_tokens 上限约束。它是最便宜的策略,因为从不调用其他模型,并且逐字保留对话尾部。v0.6.3 之前这是默认值;从 v0.6.3 开始默认是 Summarize(见上方"自动上下文压缩")。

引擎如何应用策略

每次 LLM 往返前,引擎在当前对话上调用 Context_manager.apply_strategy。函数返回(可能缩减的)对话,或在策略无法满足约束时返回 Error(例如 Truncate_oldest 达到 min_messages 但仍超 max_tokens)。

val Context_manager.apply_strategy :
  Types.context_strategy -> Types.conversation ->
  Types.llm_service option ->
  on_event:(Types.event -> unit) option ->
  (Types.conversation, Types.error_category) result

val Context_manager.estimate_tokens : Types.conversation -> int

estimate_tokens 是粗略的字符数除以四的启发式方法。它不是分词器。在推理预算时将数字视为参考值。

v0.5.1 默认值

从 v0.5.1 开始,没有显式 context_strategy 的运行时获得:

Some (Sliding_window { max_messages = 100; max_tokens = 200000 })

这是对早期 beta 版本的刻意变更,之前策略未设置让对话无限增长直到 provider 拒绝。200K token 上限匹配当前最大的生产模型,100 条消息上限防止对话在单条消息很短时依然膨胀。如果你之前依赖无限增长(例如在运行时外部自行摘要),显式设置 context_strategy = None 可恢复旧行为。

对于使用较小窗口的模型,同时降低两个数字。4K token 模型配合 max_messages = 100 仍会触发 provider 限制,因为 max_messages 在 token 估算之前检查。

取消

一等取消机制让你可以从另一个 fiber 中止正在运行的 invoke。取消是协作式的:引擎在定义的检查点检查令牌,并返回带有可恢复对话的类型化错误。

cancel_reason

type cancel_reason =
  | User_cancelled
  | Guard_cancelled of string

User_cancelled 是未提供特定原因时的默认值。Guard_cancelled 携带描述性字符串(例如,来自 observer 回调的 "loop-detector")。

取消 API

val Cancellation.create_token : Eio.Switch.t -> cancellation_token
val Cancellation.request_cancel : cancellation_token -> cancel_reason -> unit
val Cancellation.is_cancelled : cancellation_token -> bool
val Cancellation.check_cancel : cancellation_token -> unit
  (* 若已取消则抛出 Eio.Cancel.Cancelled *)
val Cancellation.reason : cancellation_token -> cancel_reason option
val Cancellation.with_timeout : float -> cancellation_token ->
  (cancellation_token -> 'a) -> ('a, [ `Cancelled | `Timeout ]) result
let token = Cancellation.create_token switch in
(* 从另一个 fiber 带原因请求取消 *)
Cancellation.request_cancel token (Guard_cancelled "loop-detector")

先到先得语义

Cancellation.request_cancel 是先到先得的:第一次调用锁定原因,后续调用被忽略。这意味着并发取消请求不会在原因上产生竞争。

取消生效点

引擎在三个点检查取消令牌:

  1. 循环顶部(每次迭代):在每次 LLM 调用前,引擎检查 Cancellation.is_cancelled token。若已取消,立即返回 Error (Cancelled reason, conversation)

  2. 每次工具分派前:若有待执行的工具调用且令牌已取消,工具不会被执行。模型收到合成结果:"[cancelled] Tool call aborted before dispatch — not executed." 这是诚实的:工具没有运行,所以对话如实说明。

  3. 每个流式 chunk:在用户回调为每个 chunk 触发后检查令牌。中途取消不会在对话中留下部分 assistant 消息(原子物化)。

诚实的限制:HTTP 层中阻塞的 C 读取仍受其自身 HTTP 超时约束。取消无法中断阻塞的 recv 调用;它在 I/O 完成后的下一个检查点生效。

取消时的返回契约

取消的运行返回:

Error (Cancelled reason, recovered_conversation)

对话始终是 provider 重放有效的:

  • 悬空的 tool_call id 会获得合成的 Tool 消息(因此对话可以无错地发回 provider)。
  • 中途中断说 "may have partially completed; verify the effect (e.g. read the file) before retrying"(副作用诚实:永远不假设无操作)。

Provider fallback 不重试取消的运行

如果 provider 调用失败且 Retry 中间件通常会重试,Cancelled 错误不可重试。取消干净地穿透中间件链。

通过 ?conversation 恢复

错误元组中的 recovered_conversation 可以通过 ?conversation 传回,以在取消点恢复或检查状态:

match Runtime.invoke rt ~agent_id:"my-agent" ~message:"Hello!"
  ~cancellation_token:token () with
| Ok result -> (* ... *)
| Error (Cancelled reason, conv) ->
  Printf.eprintf "Cancelled: %s\n"
    (match reason with
     | User_cancelled -> "user cancelled"
     | Guard_cancelled msg -> msg);
  (* conv 是重放有效的——可以传给下一次 invoke 的 ?conversation *)
  let _next = Runtime.invoke rt ~agent_id:"my-agent"
    ~message:"Continue where you left off"
    ~conversation:conv () in
  ()

单次使用令牌

取消令牌是单次使用的:重用已取消的令牌会立即取消下一次 invoke。如果需要独立的取消控制,请为每次调用创建新令牌。

Observer 干预模式

on_tool_event 回调以 unit 接收事件(仅观察)。要干预,闭包捕获取消令牌并在回调中调用 Cancellation.request_cancel

let token = Cancellation.create_token switch in
let loop_count = ref 0 in
Runtime.invoke rt ~agent_id:"my-agent" ~message:"..."
  ~cancellation_token:token
  ~on_tool_event:(fun ev ->
    incr loop_count;
    if !loop_count > 50 then
      Cancellation.request_cancel token (Guard_cancelled "loop-detector"))
  ()

这遵循 Go-context 模式:回调观察,令牌携带取消意图。

FFI 不对称:Python 回调在 OCaml 锁持有期间无法取消。从 Python 角度,dispatch-queue trio(invoke_start / invoke_poll / invoke_cancel)是取消的唯一正确隔离路径。见下方 Python 章节。

另请参阅