返回博客列表

Clark-agent:一个 Rust 写的 LLM Agent 循环,凭什么敢说 provider 无关?

2026-05-27T18:30:00+08:00
RustLLMAgent开源AI基础设施

Clark-agent:一个 Rust 写的 LLM Agent 循环,凭什么敢说 provider 无关?

最近在 GitHub 上刷到一个刚开源的 Rust 项目——clark-agent,标榜自己"provider-agnostic, sandbox-agnostic, tooling-agnostic"。通常看到这种"三个无关"的说法,我的第一反应是:又是一个 demo 级别的抽象层吧?

翻了源码之后改观了。这个库的架构设计相当克制,核心循环只有 ~400 行可读代码,但通过一套类型化的 Hook 系统(他们叫 Plugin),把 LLM 调用、工具执行、上下文变换、终止决策这些关键环节都留给了使用者。已经在 clarkchat.com 上跑了生产流量,不是玩具。

本文提纲

  1. Agent 循环到底在循环什么
  2. clark-agent 的核心架构
  3. Plugin 系统:8 种 Hook 拆解
  4. 类型系统的设计选择
  5. 跟其他 Agent 框架有什么不同
  6. 适合什么场景

Agent 循环到底在循环什么

几乎所有 LLM Agent 框架的核心都是同一个循环:

用户输入 → LLM 推理 → 解析工具调用 → 执行工具 → 结果喂回 LLM → 重复

clark-agent 把这个过程叫做 run(),循环体大概长这样:

agent_start
 └ loop:
    turn_start
    [注入 steering 消息]
    流式接收 LLM 响应
    执行工具批次(并行或串行)
    turn_end
    ↻ 直到没有工具调用且没有 steering 消息
    检查 follow-up
 agent_end

看起来没什么特别的?区别在细节里。

clark-agent 的核心架构

整个 crate 的源码结构很清晰:

文件 职责 代码量
run.rs 主循环 run() / run_continue() ~87KB
exec.rs 工具批量执行(prepare → execute → finalize) ~74KB
tool.rs Tool trait、注册表、参数校验 ~75KB
plugin.rs Plugin 系统(8 种 Hook) ~22KB
config.rs LoopConfig、Builder 模式 ~40KB
types.rs 消息、内容块、Usage 等核心类型 ~22KB
event.rs 事件系统(AgentEvent、EventSink) ~22KB

对外暴露的核心 API 只有两个函数:

pub async fn run(
    prompts: Vec,
    context: AgentContext,
    config: &LoopConfig,
    signal: CancellationToken,
) -> Result

pub async fn run_continue(
    context: AgentContext,
    config: &LoopConfig,
    signal: CancellationToken,
) -> Result

run() 启动新对话,run_continue() 从已有上下文恢复。返回值是 RunResult,包含最终消息列表和 LoopOutcome(Done / WrappedUp / HitMaxIterations)。

注意这里没有 LLM client 参数。LLM 调用被抽象成了 StreamFn——一个 trait 对象,塞在 LoopConfig 里:

pub struct LoopConfig {
    pub stream: Arc,
    pub tools: Arc,
    pub event_sink: Arc,
    // ...
}

这就是"provider 无关"的本质:你自己实现 StreamFn,不管是调 OpenAI、Anthropic 还是本地模型,clark-agent 不关心。它只管循环和编排。

Plugin 系统:8 种 Hook 拆解

这是 clark-agent 最有意思的设计。Plugin trait 定义了 8 种能力(capabilities),每个 Plugin 可以声明自己实现了哪些:

Hook 触发时机 能做什么
BeforeToolCall 工具执行前 拦截(block)或放行
AfterToolCall 工具执行后 覆盖结果、标记错误、投票终止
ContextTransform LLM 调用前 变换上下文(压缩、截断、token 预算)
EventObserver 任何事件 纯观察(日志、遥测)
SteeringSource turn 开始前 注入消息(人工介入、外部信号)
FollowUpSource 自然停止后 重新启动循环(多步骤任务)
ToolGate 每个 turn 动态工具白名单
inheritable_to_child 非 Hook 标记 Plugin 是否传递给子 Agent

这些 Hook 的组合覆盖了 Agent 开发中绝大多数定制需求。举个例子:

struct LoggingPlugin;

impl Plugin for LoggingPlugin {
    fn name(&self) -> &str { "logging" }
    fn capabilities(&self) -> PluginCapabilities {
        PluginCapabilities::EVENT_OBSERVER
    }
}

impl EventObserver for LoggingPlugin {
    fn on_event(&self, event: &AgentEvent) {
        tracing::info!(?event, "agent event");
    }
}

终止决策:unanimous-vote

工具执行后的终止逻辑用了"一致投票"机制:只有当 batch 中所有工具结果都投票 terminate = true 时,循环才会结束。这是在 AfterToolCall Hook 里实现的——任何一个 Plugin 都可以投终止票,但必须全票通过才真正停。

这个设计避免了"某个工具提前结束导致其他工具结果丢失"的问题,在并行执行场景下尤其重要。

运行中注入:Steering

SteeringSource 允许在 LLM 调用之前注入额外消息。clark-agent 内置了一个 ChannelSteering,基于 mpsc channel 实现:

let (tx, steering) = ChannelSteering::new();
// 在另一个线程里
tx.send(AgentMessage::user_text("别用搜索引擎了,直接回答")).await?;

这意味着你可以在 Agent 运行过程中实时改变它的行为——比如人工接管、根据外部事件调整策略。

类型系统的设计选择

clark-agent 在类型层面做了几件有意思的事:

消息模型是 tag-first enum

pub enum AgentMessage {
    System { content: String },
    User { content: UserContent },
    Assistant { content: AssistantContent },
    ToolResult { content: ToolResultContent },
    Custom { role: String, content: Value },
}

UserContent 支持纯文本或结构化 block(文本 + 图片),AssistantContent 区分 Text、Thinking、Reasoning、ToolCall——这些都是类型级别的区分,不是运行时字符串匹配。

Tool 的输入输出也是强类型的

Tool trait 要求实现者提供 JSON Schema(通过 schemars crate 自动推导),参数在执行前会被校验。这比"工具参数是 HashMap<String, Value>"的方式安全得多。

Budget 系统

内置了 token 预算管理(budget.rstool_result_budget.rs),支持在 ContextTransform Hook 里根据 token 限制压缩上下文。工具执行结果也有大小限制——超限会被自动截断,不会因为某个工具返回了 10MB 的输出把整个上下文搞崩。

跟其他 Agent 框架有什么不同

特性 clark-agent LangChain agent-runner
语言 Rust Python Rust
定位 库(library) 框架(framework) 可执行文件(binary)
LLM 绑定 无(你自己实现 StreamFn) 内置多种 Provider 环境变量配置
工具系统 类型化 Plugin Python 函数 内置文件系统工具
扩展方式 8 种 Hook Chain / Agent 抽象 Skills 目录
终止决策 unanimous-vote LLM 自行决定 task_done 工具
运行时依赖 几乎为零(tokio + serde) Python 生态 单二进制

关键区别:clark-agent 是一个,不是框架。它不帮你调 LLM,不提供内置工具,不做模型选择。它只做一件事:提供一个类型安全的 Agent 循环骨架,剩下的你自己填。

这意味着:

  • 你不会被框架的抽象绑架——没有 AgentChainWrapperFactory 这种东西
  • 你的 LLM 调用可以是任何方式:HTTP、gRPC、本地推理、甚至 mock
  • 你可以精确控制每个环节的行为,因为每个环节都是一个可插拔的 Hook

适合什么场景

clark-agent 的设计哲学是"只做循环,不做决策"。这种定位特别适合以下场景:

边缘设备上的 Agent。整个依赖链只有 tokioserdeasync-traittracingschemarsuuid,编译后体积可以控制在几 MB。在没有 Docker 的 ARM 设备上跑 Agent,Python 框架可能都装不上去,但 Rust 可以。

需要精细控制的高可靠性 Agent。 unanimous-vote 终止、类型化工具参数、token 预算管理——这些不是花哨的功能,是生产环境里踩过坑之后才会加的东西。clarkchat.com 已经在用,说明这些设计经历过真实流量验证。

作为其他 Agent 系统的核心引擎。如果你在做一个 Agent 平台或框架,不想自己实现循环逻辑,可以把 clark-agent 当作底层引擎。它的 Plugin 系统足够灵活,上层的任何编排策略都可以通过 Hook 实现。

不适合什么场景? 如果你想 5 分钟跑起来一个能用的 Agent,clark-agent 不是好选择——你需要自己实现 StreamFn(对接 LLM API)、自己定义 Tool、自己组装 LoopConfig。它更像是 Agent 基础设施里的"发动机",不是一辆开箱即用的"车"。


项目信息


作者: itech001 来源: 公众号:AI人工智能时代 网站: https://www.theaiera.cn/ 每日分享最前沿的AI新闻资讯和技术研究。

本文首发于 AI人工智能时代,转载请注明出处。

觉得文章不错?分享给更多人!