惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
博客园 - 聂微东
Jina AI
Jina AI
月光博客
月光博客
爱范儿
爱范儿
美团技术团队
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
T
Tailwind CSS Blog
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Apple Machine Learning Research
Apple Machine Learning Research
有赞技术团队
有赞技术团队
罗磊的独立博客
小众软件
小众软件
雷峰网
雷峰网
IT之家
IT之家
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
Visual Studio Blog

博客园 - 张善友

.NET 11 性能全解读:从 JIT 到基础库,这一版到底快了多少 .NET 11 性能深度解读:这一次,真的「到 11」了 把 LLM 密钥从环境变量里解放出来:OpenClaw.NET 迎来 Vault/OpenBao 密钥后端 RedNb.Nacos 2.0.0 正式发布:.NET 接入 Nacos 3.2.4,AI Registry 全能力落地 Rust 成为微软一线语言之后:谈谈 C# 与 Rust 的互补性 .NET 异常处理的"暗门":代码里写满 catch,你依然能抓住它——从一个 AI Agent 运行时的源码说起 写给 C++ 工程师的 OpenClaw.NET 上手指南:用你熟悉的 C++ 思维,跑起一个生产级 AI Agent .NET 11 RC1 发布:拿到"准生证",生产环境可以上了! 写给 PHP 工程师的 OpenClaw.NET 上手指南:用你熟悉的 PHP 思维,跑起一个生产级 AI Agent 写给 Rust 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Rust 思维,跑起一个生产级 AI Agent 从对标 Java 到对标 Go:Native AOT 的"无痛化"之路,走到哪一站了? NuGet 半年度总结:周下载量从 54 亿到 67 亿,.NET 生态的"新一轮增长期"实锤了 写给 Java 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Spring 思维,跑起一个生产级 AI Agent 写给 TypeScript 工程师的 OpenClaw.NET 上手指南:用你熟悉的 TS 思维,跑起一个生产级 AI Agent 写给 Golang 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Go 思维,跑起一个生产级 AI Agent MetaSkill 落地 .NET:当 Agent 从「调用工具」进化到「组织工具」 都是 AI 写代码,为什么 C# 比 Java 快半拍 MHS 三部曲(下):谁允许 AI 行动?——权力、合规与中国厂商的答卷 弱模型不能裸奔:Agent Harness 凭什么真实有效 MHS 三部曲(中):8 小时集成、六种被拦截的故障,和一次教科书级的翻车 TensorSharp 3.3.0.0 发布,视频生成、DFlash2 投机解码、安全加固一起来了 MHS 三部曲(上):别急着叫它「物理 MCP」——Anthropic 到底发布了什么 编程语言的「第三条道路」上,走得最远的其实是 C# 纯 .NET 手写 CUDA kernel,GLM-5.3-Flash decode 跑出 llama.cpp 的 2 倍 3 张卡到底能不能跑大模型推理?从 vLLM、llama.cpp 到 TensorSharp 的多卡真相 AI 编程时代,.NET 的机会在哪里? Vibe Coding 月提交量 29 亿次之后:GitHub 的危机、Azure 迁移,以及 .NET 的机会 从 PostgreSQL 到 Kubernetes:开源的护城河,从来不写在代码里 人工智能最先替代的,是人工智能学院自己 企业架构的六种场景:从"四大流派"到数字原生与 AI 原生
写给 Python 工程师的 OpenClaw.NET 上手指南:用你熟悉的 Py...
张善友 · 2026-09-05 · via 博客园 - 张善友

你已经在 Python 里玩熟了各种 Agent 框架,但当原型要走向生产——鉴权、记忆、多渠道、单二进制部署——这些脏活累活它全都内置了。

1

为什么写这篇

如果你是 Python 工程师,Agent 框架对你来说是主场:LangChain、LangGraph、AutoGen、LlamaIndex…… prototype 一晚上就能搓出来。

但原型到生产之间隔着一条鸿沟:鉴权怎么做?多渠道接入怎么管?记忆和会话怎么持久化?部署要不要拖着一个解释器和一堆依赖? 大多数 Python Agent 框架的答案是「你自己想办法」。

OpenClaw.NET 给的是另一个答案:一个用 .NET 写的自托管 AI Agent 运行时 + 网关,鉴权、策略、记忆、可观测性、多渠道接入全部内置,且能用 NativeAOT 编译成一个无依赖的单文件原生二进制——没有解释器、没有 venv、没有依赖地狱,启动即原生码。它已经开源,仓库在 github.com/clawdotnet/openclaw.net

用一句 Python 话说:

它像一个「FastAPI 应用」——对外是 HTTP / WebSocket / 各 IM 的 webhook,对内跑着一个能调工具、读写记忆、跨渠道对话的 AI Agent——只不过整个东西编译成了一个二进制。

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


一、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.*——和名字对得上,找代码不迷路。


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

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

你在 Python 里熟悉的 .NET 里的对应物
asyncio + async/await Task + async/await——概念几乎一样,写法几乎一样
asyncio.Queue System.Threading.Channels——消息管道就靠它
asyncio.Task.cancel() / 取消传播 CancellationToken(显式传参,方法最后一个参数)
pip / poetry / uv + pyproject.toml dotnet CLI + .csproj + NuGet
venv / 虚拟环境 不需要——依赖锁定在工程文件里
FastAPI / Flask ASP.NET Core(Minimal API 风格,路由写法神似 FastAPI)
dependency-injector / FastAPI Depends Microsoft.Extensions.DependencyInjection(内置 DI 容器)
@dataclass / pydantic BaseModel C# record + required
json 库 / pydantic 序列化 System.Text.Json + 源生成器(编译期) 关键差异
鸭子类型 / typing.Protocol C# interface——必须显式声明实现
Optional[str] / str | None string?——但这里是编译期强制检查
with open(...) as f: using / await using(确定性释放)
生成器 / async generator IEnumerable<T> / IAsyncEnumerable<T>(流式 token 就靠它)
pytest + unittest.mock xUnit v3 + NSubstitute
GIL(伪多线程) 真·多线程——这里的 worker 是真的并行跑的

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

// 1) 异步:和 asyncio 几乎一个模子刻出来的
public async Task<string> RunAsync(Session session, string msg, CancellationToken ct)
{
    var result = await _llm.CallAsync(msg, ct);   // ≈ await llm.call(msg),同样让出事件循环
    return result.Text;
}
// CancellationToken ≈ asyncio 的取消机制,但是显式传参风格:
// Python 里 task.cancel() 从外部砸进来,这里靠你一层层把 ct 传下去——务必传。

// 2) record + required:≈ pydantic 的 BaseModel + 必填字段,但校验发生在编译期
public sealed record OutboundMessage
{
    public required string ChannelId { get; init; }   // 不给就编译不过
    public required string RecipientId { get; init; }
}

// 3) 可空引用类型:string? 可空,string 不可空(编译器强制检查)
// ≈ 把整个项目跑在永远不许逃逸的 mypy --strict 之下
string? maybe = GetOrNull();
string sure = maybe;                // ⚠ 编译警告——本项目警告即错误!
string sure2 = maybe ?? "default";  // 用 ?? 兜底,≈ maybe or "default"(但更严格,不吞空串)

依赖注入:≈ 框架内置的 FastAPI Depends

Python 里 DI 靠 FastAPI 的 Depends 或第三方库;.NET 把 DI 容器内置了:

// 注册(≈ app.dependency_overrides[IMemoryStore] = FileMemoryStore)
services.AddSingleton<IMemoryStore, FileMemoryStore>();

// 解析(≈ 从容器取,找不到直接抛异常)
var store = sp.GetRequiredService<IMemoryStore>();

本项目几乎全是单例(Singleton),注册按职责拆成一堆扩展方法,在 Program.cs 里顺序调用——和你在 main.py 里组装依赖的拓扑结构一模一样,只是挪进了容器。

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

本项目要编译成 NativeAOT 单文件二进制,且开了激进裁剪(TrimMode=link)。Python 工程师请这样理解:

想象一个没有猴子补丁、没有 importlib 动态加载、没有运行时反射的世界——getattr(obj, name) 按字符串摸属性、__import__ 动态导模块、靠内省自动生成 schema……这些 Python 日常操作在这里都不行,因为裁剪器会把"看起来没人引用"的代码删掉。

最直接影响是 JSON 序列化:不能靠反射发现类型,要用源生成器——为类型声明 JsonSerializerContext,编译期生成序列化代码:

[JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(OperatorAccountService.StoreState))]
internal partial class GatewayJsonContext : JsonSerializerContext;

Python 视角:pydantic 平时靠反射在运行时构建模型校验器,而这里相当于强制所有模型都走「提前编译」路线——类似 pydantic v2 的预编译思路被推到了极致。你新增 DTO 时,记得挂到某个 JsonSerializerContext 上。

好消息是回报很实在:无解释器、无依赖、毫秒级冷启动——就是 PyInstaller/Nuitka 梦寐以求但很难给全的那种产物。

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

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

三、一条消息的一生

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

一条用户消息从进来到回复,共 11 步(Python 类比已标注):

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

整个系统的「骨架接口」都在 src/OpenClaw.Core/Abstractions/IToolIChannelAdapterIAgentRuntimeIMemoryStoreIToolHook……看懂它们 = 看懂系统的全部可扩展面

一个和 Python 的习惯差异:Python 是鸭子类型——「长得像鸭就是鸭」,对象有对应方法就能用;C# 是显式接口——必须写 class MyTool : ITool 声明「我实现了它」。所以要扩展,就是「声明实现某个接口 + 注册进 DI」,和你继承 BaseTool 再注册的思路一致,只是编译器会全程盯着你。

一个「源码 > 文档」的现状澄清:README 说编排器默认是 native、MAF 可选,但当前开源版本实际只跑 MAF(Microsoft Agent Framework)——native 运行时已不在仓库中,选错 orchestrator 启动会直接抛异常。把「MAF + jit」当作唯一运行时理解即可。


四、把它跑起来

前置:.NET 10 SDK(必须,≈ 装一次 Python 解释器,之后所有工程共用)、可选 Node.js 20+(仅跑 TS/JS 插件时需要)、一个 LLM API Key。

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

# 启动(≈ uvicorn main:app,但自带编译)
dotnet run --project src/OpenClaw.Gateway -c Release

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

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

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

配置体系和 Python 服务常见的「配置文件 + 环境变量覆盖」一个套路:环境变量用双下划线映射层级,OpenClaw__Runtime__Mode ↔ 配置树 OpenClaw:Runtime:Mode——≈ pydantic-settings 的 env_nested_delimiter。敏感字段支持 env:VAR_NAME 引用写法,生产环境推荐。

本地避坑速查

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

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

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

一个工具就是一个实现 ITool 的类。Python 视角:就是你继承 BaseTool 写个工具类,只不过这里接口是显式声明的:

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

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

public sealed class ReverseTextTool : ITool   // ← 显式声明实现,Python 里靠继承/鸭子类型
{
    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 安全,不走反射)≈ json.loads 后手动取字段
        using var doc = JsonDocument.Parse(argumentsJson);
        var text = doc.RootElement.GetProperty("text").GetString() ?? "";
        return new ValueTask<string>(new string(text.Reverse().ToArray()));
    }
}

然后把它加进内置工具列表(CreateBuiltInTools(...),就是个集中 new 一圈的组装函数),重启网关,对它说「reverse the text hello」——工具调用会经过完整的审计/治理/审批链路。

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

技能不是代码,而是一份「给 Agent 的操作手册」。Python 视角:工具 = 你写的 @tool 装饰器函数,技能 = 一段精心设计的 system prompt + Runbook,教 Agent 遇到某类任务怎么组合调用已有工具。

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

创建只需一个文件夹 + 一个 SKILL.md,零编译:

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

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

重启(或开热加载)即生效——比改 Python 代码还快,连解释器都不用重启。

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

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

Python 视角:前者 ≈ importlib 动态加载模块进同一进程(灵活但同居一室),后者 ≈ 起个子进程用 RPC 通信(隔离、语言自由)——连取舍都和你熟的那套一样。原生契约仅 45 行:

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 注册 → 挂适配器 → 映射端点。

webhook handler 的核心形态,写 FastAPI 的你一眼熟:

// ≈ @app.post("/webhook"):验签 → 解析 → 白名单 → 入队
public async ValueTask<WebhookResult> HandleAsync(
    string bodyText, string? signature,
    Func<InboundMessage, CancellationToken, ValueTask> enqueue, CancellationToken ct)
{
    if (_config.ValidateSignature && !IsValidSignature(bodyText, _secret, signature))
        return WebhookResult.Unauthorized();
    // ... 解析、白名单校验 ...
    await enqueue(new InboundMessage { ChannelId = "myim", SenderId = senderId, Text = text }, ct);
    return WebhookResult.Ok();
}

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

选型一图流

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

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

  1. 警告即错误TreatWarningsAsErrors=true)+ 可空性强制——≈ 把 mypy --strict + ruff --select ALL 全部调成不过就不许提交。第一次写会被编译器频繁拦,但它挡掉的就是你在 Python 里半夜被叫起来修的那类 AttributeError: 'NoneType'
  2. JSON 必须走源生成器,别指望反射式序列化(json.dumps(obj.__dict__) 的习惯在这里要改),否则 AOT 下运行时炸。
  3. 数据安全铁律:记忆/会话默认落 ./memory/,严禁用「清空整库 / DROP / 删目录」做测试隔离——只删自己创建的数据,或用独立的 throwaway 路径(≈ pytest 的 tmp_path fixture 思路)。

测试栈是 xUnit v3 + NSubstitute(≈ pytest + unittest.mock):

dotnet test                                                # 全部(≈ pytest)
dotnet test --filter "FullyQualifiedName~ProcessToolTests" # 单类(≈ pytest -k)

写在最后

对 Python 工程师来说,这套系统的学习曲线比想象中平:async/await 写法几乎照搬、异步队列、信号量、中间件链、显式依赖组装——你在 asyncio + FastAPI 里练出的那套直觉,在这里基本全部成立。

真正要适应的只有三件事:类型是编译期铁板一块(不是提示,是纪律)、接口要显式声明、以及 NativeAOT 下的「禁反射」约束。作为回报,你得到的是一个编译完就是一个二进制的生产级 Agent 运行时——没有解释器,没有依赖地狱,没有 GIL。

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

  1. src/OpenClaw.Gateway/Program.cs —— 启动主线(≈ 你的 main.py
  2. src/OpenClaw.Agent/MafAgentRuntime.cs —— Agent 循环本体
  3. src/OpenClaw.Core/Abstractions/ —— 所有可扩展接口(≈ 项目里的 base.py / abc.py

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

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