写给 Java 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Spring 思维,跑起一个生产级 AI Agent

自托管、能编译成单文件原生二进制、自带鉴权/记忆/多渠道接入的 .NET AI Agent 运行时------这篇带你用 Java 知识「无痛迁移」。

为什么写这篇

如果你是一个熟悉 Spring Boot / Maven / JUnit 的 Java 工程师,大概率没想过在 .NET 技术栈里跑 AI Agent------毕竟主流 Agent 框架几乎都是 Python / Node 优先。

OpenClaw.NET 就是来打破这个局面的 :一个用 .NET 写的自托管 AI Agent 运行时 + 网关,能编译成 NativeAOT 单文件二进制部署,自带鉴权、策略、记忆、可观测性和多渠道接入。它已经开源,仓库在 github.com/clawdotnet/openclaw.net

用一句 Java 话说:

它像一个「Spring Boot 应用」------对外是网关(HTTP / WebSocket / 各 IM 的 webhook),对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent。

本文全程用 Java 概念做类比。读完你能:看懂系统组成和消息流转、在本地把它跑起来、并写出你的第一个工具 / 技能 / 插件 / 渠道。


一、30 秒认识 OpenClaw.NET

能力 说明
网关 HTTP / WebSocket / 浏览器 UI(/chat)/ 各 IM webhook / OpenAI 兼容端点(/v1/*)/ MCP(/mcp
Agent 运行时 推理循环、工具执行、记忆、会话、技能、策略、审批、断路器
渠道 WebSocket、TG、Slack、Discord、Teams、WhatsApp,以及飞书 / 钉钉 / 企业微信
扩展点 工具(Tool)、技能(Skill)、插件(Plugin)、渠道(Channel)、LLM Provider
客户端 浏览器 UI、CLI、Avalonia 桌面 App、Blazor WASM 运维面板、TUI

💡 项目已开源:https://github.com/clawdotnet/openclaw.net。仓库里解决方案叫 OpenClaw.Net.slnx,命名空间是 OpenClaw.*------和名字对得上,找代码不迷路。


二、Java → .NET 心智模型速查

这是全文最该先读的部分。看懂这张表,后面 90% 的代码你都能读:

你在 Java 里熟悉的 .NET 里的对应物
JVM CLR(本项目可编译成 NativeAOT,启动即原生码、无 JIT
.jar / .class .dll(assembly,程序集)
Maven / pom.xml dotnet CLI + .csproj + NuGet
Spring Boot ASP.NET Core(Minimal API 风格)
@Autowired / @Bean Microsoft.Extensions.DependencyInjection
application.yml + Profile appsettings.json + 环境变量
JUnit + Mockito xUnit v3 + NSubstitute
Jackson(运行时反射) System.Text.Json + 源生成器(编译期) ⚠️ 关键差异
CompletableFuture Task<T> + async/await
Lombok @Data C# record
@Nullable / Optional 可空引用类型string? vs string(编译期强制)

语法上最容易愣住的三个点

csharp 复制代码
// 1) 异步:Task<T> ≈ CompletableFuture<T>,但 async/await 是语言级语法糖
public async Task<string> RunAsync(Session session, string msg, CancellationToken ct)
{
    var result = await _llm.CallAsync(msg, ct);   // await 不阻塞线程
    return result.Text;
}
// CancellationToken 几乎出现在每个异步方法尾参------相当于全链路的中断信号,务必往下传

// 2) record + required:≈ Lombok 不可变值对象 + 编译期必填校验
public sealed record OutboundMessage
{
    public required string ChannelId { get; init; }   // 不给就编译不过
    public required string RecipientId { get; init; }
}

// 3) 可空引用类型:string? 可空,string 不可空(编译器强制检查)
string? maybe = GetOrNull();
string sure = maybe;                // ⚠ 编译警告------本项目警告即错误!
string sure2 = maybe ?? "default";  // 用 ?? 兜底

依赖注入:和 Spring 几乎一一对应

csharp 复制代码
// 注册(≈ @Bean / @Component)
services.AddSingleton<IMemoryStore, FileMemoryStore>();

// 解析(≈ @Autowired)
var store = sp.GetRequiredService<IMemoryStore>();

@ConditionalOnMissingBean 对应 TryAddSingleton,构造器注入两边一样。丝滑过渡。

⚠️ 一个绕不开的硬约束:NativeAOT 与裁剪

这是 Java 工程师最容易踩的坑 。本项目要编译成 NativeAOT 单文件二进制,且开了激进裁剪(TrimMode=link),后果是:

  • 不能用运行时反射Class.forName()、动态代理、反射式序列化------统统不行,裁剪器会把"看起来没人引用"的类型删掉。
  • JSON 序列化用源生成器 :为类型声明 JsonSerializerContext,编译期生成代码:
csharp 复制代码
[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(OperatorAccountService.StoreState))]
internal partial class GatewayJsonContext : JsonSerializerContext;

类似 Jackson 的注解,但 .NET 是编译期生成而非运行时反射 ------更快,且裁剪安全。你新增 DTO 时,记得挂到某个 JsonSerializerContext 上。

裁剪还引出贯穿全文的两条运行时车道

  • aot 车道:裁剪安全、低内存、无动态加载。生产 Docker 镜像走这条。
  • jit 车道:完整 .NET,支持反射和进程内动态加载插件。开发期默认走这条。

三、一条消息的一生

这是理解整个系统的主线。中枢是 OpenClaw.Gateway------它在启动时把 Agent 运行时、消息管道、渠道适配器、插件宿主组合起来,统一路由所有流量。

一条用户消息从进来到回复,共 11 步:

  1. 渠道收消息IChannelAdapter 把入站消息写进 MessagePipeline(基于 System.Threading.Channels,≈ 有界 BlockingQueue
  2. Worker 取消息:1~4 个 worker(上限 = CPU 核数)从队列读
  3. 会话加锁 :拿该会话的 SemaphoreSlim,同一会话不并发跑两轮
  4. 过中间件:限流、token 预算,可短路拒绝
  5. 进 Agent 运行时MafAgentRuntime.RunAsync(...)
  6. 准备上下文:载入/新建会话、裁剪历史、注入记忆召回
  7. ReAct 循环:调 LLM → 要工具就执行 → 结果回灌 → 再调 LLM......直到产出文本
  8. 工具执行:一条完整链路------预设过滤 → 治理策略 → Hook → 人工审批 → 执行 → 审计
  9. 韧性:LLM 调用自带指数退避重试、超时、断路器、降级模型级联
  10. 落库 :会话写入 IMemoryStore(dev 默认 sqlite)
  11. 回复出站 :按 ChannelId 找到渠道适配器投递

整个系统的「骨架接口」都在 src/OpenClaw.Core/Abstractions/IToolIChannelAdapterIAgentRuntimeIMemoryStoreIToolHook......看懂它们 = 看懂系统的全部可扩展面。要扩展,就实现接口、注册进 DI------和 Spring 的 SPI 思路完全一致。


四、把它跑起来

前置:.NET 10 SDK(必须)、可选 Node.js 20+(仅跑 TS/JS 插件时需要)、一个 LLM API Key。

bash 复制代码
# 先校验配置(≈ Spring 启动前自检)
dotnet run --project src/OpenClaw.Gateway -c Release -- --doctor

# 启动
dotnet run --project src/OpenClaw.Gateway -c Release

默认监听 http://127.0.0.1:18789,浏览器打开 /chat 即可对话。

最快的本地启动(三个环境变量 + 一条命令):

bash 复制代码
export MODEL_PROVIDER_KEY="sk-..."          # 你的 LLM key
export OPENCLAW_WORKSPACE="$PWD/workspace"  # 工作区根目录
mkdir -p "$OPENCLAW_WORKSPACE"
dotnet run --project src/OpenClaw.Gateway -c Release

本地避坑速查

  • 必须 .NET 10,多 SDK 并存时用 global.json 钉版本
  • 出厂 appsettings.json 里的默认 AuthToken 和示例 API key 仅供本地回环 ,对外部署务必改成 env: 引用,别把真实密钥提交进仓库
  • 公网绑定会被安全硬化拦截(缺鉴权 token、危险工具、raw: 密钥都会拒绝启动)------这是有意设计

五、动手扩展:四种方式,从轻到重

① 写一个工具(Tool)------ 最常用

一个工具就是一个实现 ITool 的类,接口极简:

csharp 复制代码
public interface ITool
{
    string Name { get; }              // LLM 用它来调用
    string Description { get; }       // 决定 LLM 何时调用它
    string ParameterSchema { get; }   // 参数的 JSON Schema
    ValueTask<string> ExecuteAsync(string argumentsJson, CancellationToken ct);
}

最小可用示例(字符串反转工具):

csharp 复制代码
public sealed class ReverseTextTool : ITool
{
    public string Name => "reverse_text";
    public string Description =>
        "Reverse the characters of the given text.";

    public string ParameterSchema => """
    {
      "type": "object",
      "properties": {
        "text": { "type": "string", "description": "Text to reverse" }
      },
      "required": ["text"]
    }
    """;

    public ValueTask<string> ExecuteAsync(string argumentsJson, CancellationToken ct)
    {
        // 用 JsonDocument 解析入参(AOT 安全,不走反射)
        using var doc = JsonDocument.Parse(argumentsJson);
        var text = doc.RootElement.GetProperty("text").GetString() ?? "";
        return new ValueTask<string>(new string(text.Reverse().ToArray()));
    }
}

然后把它加进内置工具列表(CreateBuiltInTools(...)),重启网关,对它说「reverse the text hello」------工具调用会经过完整的审计/治理/审批链路。

② 写一个技能(Skill)------ 最轻,纯 Markdown

技能不是代码 ,而是一份「给 Agent 的操作手册」。Java 视角:工具 = 可执行的 @Service,技能 = 一份 Runbook,教 Agent 遇到某类任务怎么组合使用已有工具。

机制是渐进式披露:系统提示里只放技能索引(省 token)→ Agent 判断相关时拉取完整正文 → 需要时再读附属文件。

创建只需一个文件夹 + 一个 SKILL.md

markdown 复制代码
---
name: pr-reviewer
description: 当用户要求审查一个 Pull Request 或 diff 时使用。
---

## 步骤
1. 用 `read_file` 或 `shell`(git diff)拿到改动
2. 按正确性、边界、安全、可读性审查
3. 输出分级意见:🔴 必须改 / 🟡 建议 / 🟢 可选

无需编译,重启(或开热加载)即生效。

③ 写一个插件(Plugin)------ 打包一组能力

两条路:原生 .NET 动态插件 (进程内 DLL 加载,仅 jit 车道)和 JS/TS 桥接插件(Node.js 子进程 + JSON-RPC,两条车道都行)。原生契约仅 45 行:

csharp 复制代码
public sealed class MyPlugin : INativeDynamicPlugin
{
    public void Register(INativeDynamicPluginContext context)
    {
        context.RegisterTool(new ReverseTextTool());
        // 还能 RegisterChannel / RegisterHook / RegisterProvider ...
    }
}

④ 接一个渠道(Channel)------ 接你自己的 IM

契约是 IChannelAdapter(收 + 发),入站走「webhook → handler 校验解析 → 管道入队」,出站按 ChannelId 路由投递。照抄 Twilio SMS 的实现(最简单的参照)即可,6 步:配置类 → 适配器 → webhook handler → DI 注册 → 挂适配器 → 映射端点。

🔒 每个渠道都该有的安全面:签名校验(恒定时间比较)、发送者白名单、体积上限、去重窗口。

选型一图流

你的需求 要编译吗
加一个 Agent 能调用的动作 工具
教 Agent 某类任务的处理流程 技能 不要(纯 md)
打包一组能力 / 复用 TS 生态 插件 原生要 / 桥不要
接一个新的消息入口 渠道

六、开发约定:三个必须知道的红线

  1. 警告即错误TreatWarningsAsErrors=true)+ 可空性强制------第一次写会被编译器频繁拦,但能挡掉一大类 NPE。
  2. JSON 必须走源生成器,别依赖反射式序列化,否则 AOT 下运行时炸。
  3. 数据安全铁律 :记忆/会话默认落 ./memory/,严禁用「清空整库 / DROP / 删目录」做测试隔离------只删自己创建的数据,或用独立的 throwaway 路径。

测试栈是 xUnit v3 + NSubstitute:

bash 复制代码
dotnet test                                                # 全部
dotnet test --filter "FullyQualifiedName~ProcessToolTests" # 单类

写在最后

对 Java 工程师来说,这套系统最大的亲和力在于:它就是「接口 + DI 注册 + 启动时组装」,整套心智模型和 Spring 一脉相承。真正的门槛只有一个------NativeAOT 裁剪带来的「禁反射」约束,迈过去就是一马平川。

想继续深挖,最可靠的三个源码入口:

  1. src/OpenClaw.Gateway/Program.cs ------ 启动主线
  2. src/OpenClaw.Agent/MafAgentRuntime.cs ------ Agent 循环本体
  3. src/OpenClaw.Core/Abstractions/ ------ 所有可扩展接口

本文所有代码与结论均对照开源仓库 clawdotnet/openclaw.net 当前源码(双运行时 = AgentRuntime/MAF + jit)。如果你发现与代码不符------以代码为准,也欢迎提 PR。

觉得有用,欢迎去 GitHub 点个 Star ⭐,也欢迎点赞 / 在看 / 转发给你的 Java 朋友 👋