2026年3月

已知新出的 xdr studio 显示器内部用的 a19pro 12G 内存,128Gc 闪存,全方位吊打 macbook neo 。

这么强的配置只是用来跑色彩管理,tb4 连接,摄像头驱动等底层功能,不知道该说是杀鸡用牛刀呢还是三哥软件写的太张扬

坐标宁波
现在用的手机电信套餐 91 元含 4 个副卡 无限但超过 40G 降速的流量 300M 的宽带
不知道这个套餐目前还算不算划算

前两年当地营业厅给我提到 500M
这几天给降到 300M 了 去营业厅说活动没了
有啥方法能提高点宽带?

每周一个小惊喜,mx anywhere 3s 已经送出 2 个,本周回帖抽 JBL GO4 ,踏春挂在背包上很拉风。。。


免五低佣低两融开户,找老倔驴,靠谱! 开户选择多还靠谱,聚合几十家券商优惠,连接 100+家营业部,已帮助 2000 人+。

开户点这里: https://jue.lv/kh/V2/202603b (10+券商可选,开户有礼!)

  • 只玩 ETF 的聪明钱 银河 ETF 万 0.5 ,1 毛起收非常合适。适合大多数散户,宽指 ETF+逆回购+打新。
  • 量化选手 miniqmt 申万国金可选。
  • 50W 以上的大佬 多个国泰营业部可选,还有长江。
  • 两融 西南广发银河平安等 50w-3.5%,100w-3.3%,200w-3.1%,800w-2.88%
  • VIP 通道 东莞万 0.76 ,20 万获得上限 50 人的 VIP 通道(不限制人数的都是假的,一般需要 100 门槛的),长江高门槛上限 200 人尊享通到,打板领先一步;

另外,渣打远程开港卡(存刀年化 3.7%,还能远程开港卡),港美 FX 可选;

(低门槛最近有点拉跨,需要 2 万低门槛又股票免五只能所以说有机会能开赶紧开)

友情提醒:不因奖品而参与股市,得不偿失!

为什么你的龙虾 openclaw 搜索网络资讯的技能不好用

前言

最近小伙伴在使用龙虾 openclaw 执行一些搜索网络资讯的服务时,发现被限制了频率。

图片

这个问题一方面可能是你对接的大模型的套餐的问题,就是超出套餐的频率了,比如最近比较多的一些 coding plan 套餐,一般都是 5 个小时 1200 次,超过了就会限速。

图片

另外一方面是因为 openclaw 内置的 web 工具有两个

  • 一个是 web\_search:默认使用 Brave Search API,它会根据搜索内容帮你整理并且返回,比较智能和高效,目前是需要付费使用的。

图片

  • 另一个是 web\_fetch:使用 http 协议,相当于直接打开网页,但是网页内容还需要龙虾进一步提炼和分析。

所以到这里你就知道为什么你的龙虾搜索网络资讯时,为什么不好用了吧?

解决方案

可以使用 Tavily Web 替代方案,它针对登录用户每月提供了 1000 Credits ,基本可以够普通用户体验使用。

图片

获取 key

注册和登录

然后获取你的专属的key

tvly-dxxxxxxxx

图片

使用和配置

然后回到你的小龙虾上,这样告诉它

帮我安装 Tavily Web 这个技能,并且设置它为你默认的搜索技能,并且配置 Tavily Web 的 key: xxxxxxxxx

上面的 xxx 需要转换成你的 key

image-20260311191914124

image-20260311191914124

图片

根据提示做回应即可,还是比较智能的。

完工

现在你可以继续使用联网的搜索服务了。

图片

总结

万少建立了一个专门讨论小龙虾OpenClaw的群,大家可以互相交流和分享关于小龙虾生态的资讯消息。

关注我,持续分享鸿蒙开发 + AI 提效的实战技巧。

点赞 + 关注 + 收藏 = 学会了

💡整理了一个 NAS 专属玩法专栏,感兴趣的工友可以戳这里关注 👉 《NAS邪修》

CoPaw 是阿里云通义团队于2026年2月14日推出的个人智能助理,支持本地与云端双模式部署,并于2026年2月28日开源。它不仅支持多种主流大模型(LLM),最核心的卖点在于其强大的扩展性,支持 MCP(Model Context Protocol)、自定义工具、技能,并能通过“频道”功能无缝对接钉钉、飞书、QQ 等平台,是打造私有 AI 自动化的利器。

本次教程以 飞牛 NAS 为例进行演示,其他品牌 NAS(如群晖、绿联等)操作逻辑基本一致。

打开 NAS 的「文件管理」,找到 docker 文件夹,在其内部新建一个名为 copaw 的文件夹。

再在 copaw 里创建一个 data 文件夹。

打开 NAS 的「Docker」应用,切换到 「Compose」 面板,点击“新增项目”。

  • 项目名称:copaw
  • 路径:选择刚刚创建的 docker/copaw 这个文件夹
  • 来源:创建docker-compose.yml

在新建的 docker-compose.yml 中,复制粘贴以下代码,保存即可:

services:
  copaw:
    image: agentscope/copaw:latest
    container_name: copaw
    ports:
      - 6688:8088 # 6688 可根据本地端口占用情况自定义
    volumes:
      - ./data:/app/working
    restart: unless-stopped

点击保存并等待容器构建完成。在浏览器访问 NAS_IP:6688 即可进入 CoPaw 界面。

打开 CoPaw 第一件事当然是把界面设置成中文啦。

点开右上角菜单可以切换成「简体中文」

CoPaw 作为一款个人助理工具,肯定得给它配个大模型。

我以月之暗面(Kimi)为例,你可以自行选择。

点击左侧菜单栏的 「模型」 -> 「添加提供商」,填入相关配置信息。kimi 的 Base URL 是 `https://api.moonshot.cn/v1。请根据你使用的提供商来填。

新增完“提供商”后,在「模型」页面往下滑动就可以看到刚刚新增的 kimi 了。

点击 kimi 卡片的「设置」按钮,配置一个 API 密钥(API Key,请到你使用的模型提供商那里申请),配置完后点击一下「测试连接」按钮,如果页面顶部出现 Connection successful 提示证明连通了~

连通之后点击「保存」。

连通之后,点击 kimi 卡片的「模型」按钮,配置一个模型。我用的是 kimi-k2.5

同样,添加模型后点击一下「测试连接」按钮,页面顶部出现 Connection successful 证明可以使用该模型了。

此时回到「模型」页面的顶部,配置一下LLM。

模型提供商选择刚刚创建的 kimi,模型选择刚刚配置的 kimi-k2.5,然后点击一下右侧的「保存」按钮。

回到首页的「聊天」页面就可以开始聊天了。

但只能聊天就不是AI助理了。

在左侧菜单栏可以看到 CoPaw 可以配置技能、工具、MCP,在「频道」面板还能接入钉钉、飞书、QQ 等工具。也就是类似小龙虾的玩法了。

至于 CoPaw 能做什么,请参考 CoPaw 官方文档(https://copaw.agentscope.io/docs/intro)或者现在网上讨论得热火朝天的小龙虾(OpenClaw)。

既然聊到 OpenClaw 了,那 CoPaw 和 OpenClaw 又有什么区别呢?

详情请看 CoPaw 官方文档的介绍:https://copaw.agentscope.io/docs/comparison


以上就是本文的全部内容啦,你有好玩的镜像推荐吗?欢迎在评论区留言讨论!

想了解更多NAS玩法记得关注《NAS邪修》👏

往期推荐:

点赞 + 关注 + 收藏 = 学会了

我个人观点是,经济下滑只是一个方面,确实没有那么多需求要做,大量公司有减员的需求,但是 AI 对我们开发者的冲击远比很多人想象的要大。

在过去没有 AI 的时代,确实没有当前这种效率,为了压缩工期,只能加人,然后加一个人,增加更多的沟通成本,反而降低了整体效率,这一点在人月神话中早就指出,这就是焦油坑战术,最后怎么都摆脱不了,只能反复延期。

我在过去的工作中,光是跟前端对接,不知道浪费了多少口水,写一堆文档,对方看也不想看,当面讲一堆接口调用说明,无论多么细致,最后联调的时候发现,最担心的问题还是会发生,前端调用你的接口该出问题,还是会出问题,光是沟通就要耗费大量的时间,现在大量的页面都不要人来写了,opus 写好直接对接后端,我不知道单独设立前端这个工种的意义,除了降低开发效率,增加沟通成本之外。

而且我的领导还跟我们说现在很难做到像素级别还原,很多人还是不能替代的,但是从成本的角度来讲,如果 AI 写的页面只有人类开发者的 30%的成本,老板可能觉得页面有点错误也是可以接受的,毕竟页面大部分时候并不会对业务核心造成致命影响。

而且前端目前出了小问题,后端自己修修补补就好了,实在搞不定,留个前端专家擦兜底即可,公司留那么多不懂业务流程的前端开发干什么,我真的为前端这个独立工种感到深深担忧。

从后端来看,大量的简单的 CRUD ,已经没有任何价值,后端的出路在于业务系统的整体分析,跟模块拆分,当前 LLM 在大量上下文下还是容易产生幻觉,未来的方向肯定是往细分化模块去拆,人类开发者提供接口定义说明与规格,让每一个小的模块由 AI 来完成,最后由人类开发者来审核单测,并最终参与代码模块集成,而开发写代码本身这个工作,越来月没有价值。

后端应该从产品、架构、代码管理者的角度去思考问题,如何让项目更容易让 AI 来完成,核心工作绝对不再是搞定摸个特定模块的编码,这样的技能没有任何价值。

而且目前对于中小团队来讲,AI 全栈、全流程化是非常有吸引力的,一个人既是产品、又是测试、开发、运维的超级个体。

在当前 AI 的时代,这些技能并不会对个人造成学习负担,过去我们认为全干工程师,什么都干不好,但是今天情况发生了颠覆性的变化,LLM 里面蕴藏着大量人类开发者十数年的经验,只要运用得当,一个人当一个多面手根本不是问题。

最后,我觉得过多的人反而是一种负担,3 个老手+高效的 CodeAgent ,实现外科手术式精英开发团队干翻 20 个人的传统开发团队根本不是问题,有 CodeAgent 的今天,整个开发团队人数太多真的是一种负担。

你带个新人,光是讲我们的 git flow 就要费半天的功夫,我写几行提示词跟 SKILLS ,Agent 给我把 git flow 流程干的明明白白。

资深开发者在关键节点审查一下即可,为什么要招实习生呢?大量的初级开发者根本没有存在的必要。

这一点在北美招聘市场已经有体现了,大量公司只要成熟资深的开发者,而初级开发者的招募动作基本停滞。

简介

.NET 项目里,很多“重复但又不能随便写错”的代码,本质上都不值得手写。

例如:

  • DTO 映射代码;
  • INotifyPropertyChanged 模板代码;
  • 接口注册代码;
  • 序列化上下文;
  • 特性驱动的样板方法;
  • 各种“按规则扫描代码再生成辅助类”的场景。

过去这类问题通常有 3 种解法:

  • 手写,最直接,但重复劳动多;
  • 运行时反射,灵活,但有开销;
  • T4 / 脚本 / CLI 代码生成,能用,但维护成本高。

Source Generator 的出现,本质上就是给这类问题一个更现代的答案:

在编译期间分析你的代码,并自动生成新的 C# 源文件,让它们和手写代码一起参与编译。

这就是为什么源生成器看起来像“高级编译器功能”,但实际项目里它非常务实。

Source Generator 到底是什么?

Source GeneratorRoslyn 编译器扩展机制的一部分。

它做的事情可以简化成:

你的源码 -> Roslyn 分析语法树和语义信息
         -> Source Generator 读取这些信息
         -> 生成新的 .cs 代码
         -> 编译器把手写代码和生成代码一起编译

这意味着几个关键点:

  • 它运行在编译期,不是运行期;
  • 它不会修改你原来的文件;
  • 它只能“新增源代码”,不能直接篡改已有代码;
  • 生成出来的代码会参与编译、报错、补全和类型检查。

所以你可以把它理解成:

  • 不是“运行时代码注入”;
  • 而是“编译时代码补齐”。

为什么源生成器值得学?

因为它解决的是一类很典型、很现实的问题:

  • 手写太机械;
  • 反射太慢或不利于 AOT;
  • 手工同步容易出错;
  • 模板代码和业务代码长期分离后容易失真。

几个非常典型的场景:

场景Source Generator 能做什么
System.Text.Json生成序列化元数据,减少反射,提高性能
DI 自动注册扫描标记类型并生成注册代码
DTO / Mapper根据特性生成映射器或 DTO
INotifyPropertyChanged根据字段或属性生成通知逻辑
Native AOT生成静态可见代码,减少运行时反射依赖
SDK/客户端生成根据协议或元数据生成强类型代码

一句话总结:

  • 运行时反射更灵活;
  • 编译时源生成器更静态、更快、更适合类型安全和 AOT。

Source Generator 和反射、Expression Tree、Source Generator 之外的代码生成,有什么区别?

这几个东西经常被混在一起。

和反射的区别

  • 反射是在运行时读取类型信息;
  • 源生成器是在编译时分析代码并输出静态代码。

如果你的需求是“运行时再决定做什么”,反射仍然有价值。

如果你的需求是“编译时就知道规则,想生成固定代码”,源生成器通常更合适。

和表达式树的区别

  • 表达式树主要解决“把代码表示成对象结构,并在运行时分析或拼接”;
  • 源生成器主要解决“在编译时生成新的源码”。

两者都和“代码生成”有关,但时间点完全不同。

和 T4 / 手工脚本生成的区别

  • T4、脚本、命令行模板常常是编译流程之外的外部步骤;
  • 源生成器直接集成进 Roslyn 编译流程里。

这带来的优势很直接:

  • 不容易忘记重新生成;
  • IDE 体验更统一;
  • 类型系统和错误提示更自然。

源生成器的工作方式

如果只记一条主线,记这个就够了:

源码 -> 语法树 -> 语义分析 -> Generator 执行 -> AddSource -> 合并编译

源生成器能拿到的核心信息包括:

  • SyntaxTree:语法树
  • SemanticModel:语义模型
  • Compilation:当前项目编译上下文
  • INamedTypeSymbol / ISymbol:类型、方法、属性等符号信息

这些信息足够你回答很多问题:

  • 哪些类带了某个特性?
  • 某个属性是什么类型?
  • 这个类是不是 partial
  • 这个命名空间是什么?
  • 这个接口是否被实现?

然后你就可以决定生成什么代码。

两种源生成器:ISourceGeneratorIIncrementalGenerator

这是今天写源生成器最重要的一个区分。

1. 传统生成器:ISourceGenerator

接口大致长这样:

public interface ISourceGenerator
{
    void Initialize(GeneratorInitializationContext context);
    void Execute(GeneratorExecutionContext context);
}

它的特点:

  • 模型直观;
  • 容易理解;
  • 适合教学和简单场景;
  • 但每次编译往往是全量思维,容易重复计算。

2. 增量生成器:IIncrementalGenerator

接口大致长这样:

public interface IIncrementalGenerator
{
    void Initialize(IncrementalGeneratorInitializationContext context);
}

它的特点:

  • 通过增量管道组织输入和输出;
  • 只有相关输入发生变化时才重新计算;
  • 更适合真实项目和大型代码库;
  • 编译性能通常更好。

今天的务实建议非常明确:

  • 想理解原理,可以先看 ISourceGenerator
  • 真正在项目里写新生成器,优先考虑 IIncrementalGenerator

一个最小可运行示例:生成 HelloWorld 类

先看最小版本,这样最容易建立直觉。

第一步:创建生成器项目

通常创建一个类库项目:

dotnet new classlib -n MySourceGenerator

项目文件可以这样配置:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>latest</LangVersion>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" PrivateAssets="all" />
  </ItemGroup>
</Project>

之所以通常选 netstandard2.0,是因为它对生成器项目的兼容性最好。

第二步:写一个最简单的生成器

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;

[Generator]
public sealed class HelloWorldGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
        const string source = """
namespace Generated;

public static class HelloWorld
{
    public static string SayHello() => "Hello from Source Generator";
}
""";

        context.AddSource(
            "HelloWorld.g.cs",
            SourceText.From(source, Encoding.UTF8));
    }
}

这里最关键的一行是:

context.AddSource("HelloWorld.g.cs", ...);

它的意思不是“写磁盘文件”,而是“把这段源码加入当前编译”。

第三步:在业务项目里引用它

业务项目里引用生成器时,重点不是普通程序集引用,而是作为 analyzer 引入:

<ItemGroup>
  <ProjectReference Include="..\MySourceGenerator\MySourceGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

然后你就可以在业务代码里直接使用生成出来的类型:

Console.WriteLine(Generated.HelloWorld.SayHello());

为什么很多生成器都要求 partial

这是非常常见的模式。

例如你会看到用户代码写成:

[AutoNotify]
public partial class Person
{
    private string _name = string.Empty;
}

生成器再额外生成:

public partial class Person
{
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name));
        }
    }
}

之所以用 partial,是因为:

  • 手写代码和生成代码本质上是同一个类型的不同部分;
  • 生成器不能直接改你原来的类;
  • 所以最自然的办法就是生成另一个 partial 片段。

这也是大多数源生成器设计的基本套路。

从“写死生成”走向“按规则生成”

真正有意义的生成器,当然不是每次都无脑输出一个固定类,而是:

  • 扫描代码中感兴趣的目标;
  • 分析它们的符号信息;
  • 再按规则生成对应代码。

例如:扫描带 [GenerateToString] 特性的类。

用户代码:

[GenerateToString]
public partial class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
}

生成器逻辑大致要做这些事:

  • 找到所有类声明;
  • 判断类上是否有这个特性;
  • 获取属性列表;
  • 生成 ToString() 或辅助方法。

这时候,语法树和语义模型就派上用场了。

传统生成器的典型写法

先看 ISyntaxReceiver 版本,因为它最容易理解。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

[Generator]
public sealed class DemoGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
    }

    public void Execute(GeneratorExecutionContext context)
    {
        if (context.SyntaxReceiver is not SyntaxReceiver receiver)
        {
            return;
        }

        foreach (var classNode in receiver.CandidateClasses)
        {
            var model = context.Compilation.GetSemanticModel(classNode.SyntaxTree);
            var symbol = model.GetDeclaredSymbol(classNode);

            if (symbol is null)
            {
                continue;
            }

            // 这里可以继续判断特性、命名空间、成员等
        }
    }
}

internal sealed class SyntaxReceiver : ISyntaxReceiver
{
    public List<ClassDeclarationSyntax> CandidateClasses { get; } = new();

    public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
    {
        if (syntaxNode is ClassDeclarationSyntax classNode)
        {
            CandidateClasses.Add(classNode);
        }
    }
}

这里的逻辑可以拆成两步:

  • SyntaxReceiver 先粗筛:哪些语法节点值得关注;
  • Execute 再结合语义模型细筛:这些类到底是不是目标类型。

这是传统生成器最经典的写法。

增量生成器为什么更值得用?

因为真实项目里,编译性能很重要。

如果每次小改一个文件,你的生成器都全量重新扫描和全量重新拼接代码,IDE 体验会明显变差。

增量生成器的核心思路是:

  • 把“发现目标”
  • “提取信息”
  • “生成代码”

拆成一条增量数据管道。

只有输入变化的部分,才会重新计算。

一个增量生成器的最小示例

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

[Generator]
public sealed class ClassNameListGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var classDeclarations = context.SyntaxProvider.CreateSyntaxProvider(
            predicate: static (node, _) => node is ClassDeclarationSyntax,
            transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node);

        context.RegisterSourceOutput(classDeclarations, static (spc, classNode) =>
        {
            var className = classNode.Identifier.Text;

            var source = $$"""
namespace Generated;

public static class {{className}}Info
{
    public const string Name = "{{className}}";
}
""";

            spc.AddSource($"{className}.Info.g.cs", SourceText.From(source, Encoding.UTF8));
        });
    }
}

虽然这个例子很简单,但它已经体现了增量生成器的基本风格:

  • 先声明输入源;
  • 再定义转换过程;
  • 最后注册输出。

一个更接近实战的例子:根据特性生成方法

下面这个示例会稍微更真实一些。

目标是:

  • 让用户给类打一个 [GenerateGreeting] 特性;
  • 生成器自动为这个类补一个 SayHello() 方法。

用户侧代码

namespace Demo;

[GenerateGreeting]
public partial class UserService
{
}

特性定义

using System;

[AttributeUsage(AttributeTargets.Class)]
public sealed class GenerateGreetingAttribute : Attribute
{
}

增量生成器

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

[Generator]
public sealed class GreetingGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        var candidates = context.SyntaxProvider.CreateSyntaxProvider(
            predicate: static (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,
            transform: static (ctx, _) =>
            {
                var classNode = (ClassDeclarationSyntax)ctx.Node;
                var symbol = ctx.SemanticModel.GetDeclaredSymbol(classNode) as INamedTypeSymbol;
                return symbol;
            })
            .Where(static symbol => symbol is not null);

        context.RegisterSourceOutput(candidates, static (spc, symbol) =>
        {
            if (symbol is null)
            {
                return;
            }

            var hasAttribute = symbol.GetAttributes()
                .Any(a => a.AttributeClass?.Name == "GenerateGreetingAttribute");

            if (!hasAttribute)
            {
                return;
            }

            var namespaceName = symbol.ContainingNamespace.IsGlobalNamespace
                ? null
                : symbol.ContainingNamespace.ToDisplayString();

            var source = $$"""
{{(namespaceName is null ? "" : $"namespace {namespaceName};")}}

public partial class {{symbol.Name}}
{
    public string SayHello()
    {
        return "Hello from generated code";
    }
}
""";

            spc.AddSource($"{symbol.Name}.Greeting.g.cs", SourceText.From(source, Encoding.UTF8));
        });
    }
}

这个例子虽然简单,但已经足够说明大部分业务生成器的核心套路:

  • 通过特性声明意图;
  • 生成器扫描这些特性;
  • 再生成 partial 代码补到目标类型上。

这段增量生成器代码,逐行到底在做什么?

很多人第一次看增量生成器,不是卡在“概念”,而是卡在这几个 API 名字:

  • CreateSyntaxProvider
  • predicate
  • transform
  • RegisterSourceOutput

它们看起来很像编译器黑话,但其实这段代码做的事情非常朴素:

从所有语法节点里找出“带特性的类”,拿到它们的类型符号,再判断是不是带了目标特性,如果是,就生成代码。

可以把整段逻辑先压缩成一条流水线:

所有语法节点
-> 粗筛出“带特性的类”
-> 把类语法节点转成类型符号
-> 过滤空值
-> 检查是否真的带目标特性
-> 生成对应的 partial 代码

下面按执行顺序逐行拆。

using 部分

using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;

作用分别是:

  • System.Text:主要为了 Encoding.UTF8
  • Microsoft.CodeAnalysis:Roslyn 核心 API,比如 IIncrementalGeneratorINamedTypeSymbol
  • Microsoft.CodeAnalysis.CSharp.Syntax:C# 语法节点类型,比如 ClassDeclarationSyntax
  • Microsoft.CodeAnalysis.TextSourceText,用于把字符串包装成编译器接受的源码对象

[Generator]

[Generator]

这个特性告诉 Roslyn:

  • 这个类是一个源生成器
  • 编译器需要在编译阶段加载并执行它

没有这个标记,编译器不会把它当作生成器。

类声明

public sealed class GreetingGenerator : IIncrementalGenerator

这里有两个重点:

  • 这是一个源生成器
  • 它使用的是增量模型 IIncrementalGenerator

这意味着它不是每次编译都用“全量扫描 -> 全量生成”的思路,而是把处理过程拆成若干增量步骤,只在输入变化时重新计算对应部分。

Initialize(...)

public void Initialize(IncrementalGeneratorInitializationContext context)

增量生成器只需要实现这一个方法。

但这里要注意,它不是“直接开始生成代码”的方法,而更像是:

  • 定义输入从哪里来
  • 定义输入如何筛选和转换
  • 定义最后如何产出生成代码

也就是说,Initialize 本质上是在搭一条增量处理管道。

第 1 步:建立候选输入 candidates

var candidates = context.SyntaxProvider.CreateSyntaxProvider(

这行可以理解成:

  • 从编译器能看到的所有语法节点中,构建一个“候选数据流”
  • 之后这条数据流会不断产出可能和生成逻辑有关的节点

这里的 candidates 不是最终结果,而是后续生成代码要用的一批候选对象。

predicate

predicate: static (node, _) => node is ClassDeclarationSyntax cds && cds.AttributeLists.Count > 0,

这一行是在做“粗筛”。

它的意思是:

  • 如果当前语法节点是一个类声明 ClassDeclarationSyntax
  • 并且这个类上写了特性列表
  • 那它就是候选项

注意,这里并没有判断:

  • 它是不是带了 GenerateGreetingAttribute

这里只是先把完全不可能相关的节点排掉,比如:

  • 方法
  • 属性
  • 接口
  • 没有任何特性的类

这样做的原因很简单:

  • predicate 阶段要尽量便宜
  • 它适合做快速语法级筛选
  • 不适合在这里做太重的语义分析

再看这个 static

static (node, _) => ...

它表示这个 lambda 不捕获外部变量,在增量生成器里这样写更常见,也更利于性能。

第二个参数 _ 这里没用,所以直接忽略。

transform

transform: static (ctx, _) =>
{
    var classNode = (ClassDeclarationSyntax)ctx.Node;
    var symbol = ctx.SemanticModel.GetDeclaredSymbol(classNode) as INamedTypeSymbol;
    return symbol;
})

这一步是在做“从语法节点到语义符号”的转换。

前面的 predicate 只是告诉我们:

  • 这个节点看起来像一个“带特性的类”

但这里才真正把它变成可以深入分析的类型符号。

先看第一行:

var classNode = (ClassDeclarationSyntax)ctx.Node;

因为前面的 predicate 已经保证当前节点是类声明,所以这里可以安全转成 ClassDeclarationSyntax

你可以把 classNode 理解成:

  • 代码长相层面的类节点

它知道:

  • 这是一个类
  • 类名是什么
  • 写了哪些特性语法

但它还不是“真正的类型定义对象”。

再看第二行:

var symbol = ctx.SemanticModel.GetDeclaredSymbol(classNode) as INamedTypeSymbol;

这行非常关键。

它做的事是:

  • 根据语义模型,把类语法节点转换成类型符号 INamedTypeSymbol

为什么一定要转成 symbol

因为很多真正重要的信息,语法节点本身并不适合直接判断,例如:

  • 这个类完整命名空间是什么
  • 它身上的特性到底是什么真实类型
  • 它有哪些属性、方法、基类、接口
  • 它是不是泛型类

这些通常都更适合从 INamedTypeSymbol 读取。

所以到这一步,生成器已经从“看代码长相”升级到“理解代码语义”了。

最后:

return symbol;

意味着后续增量管道里流动的,不再是类语法节点,而是类的类型符号。

.Where(...)

.Where(static symbol => symbol is not null);

这是在做空值过滤。

因为 GetDeclaredSymbol(...) 理论上可能拿不到结果,所以这里把空值剔掉。

经过这一步之后,candidates 可以理解成:

  • 一串非空的候选类类型符号

第 2 步:把候选项变成真正的源代码输出

context.RegisterSourceOutput(candidates, static (spc, symbol) =>

这一行的意思是:

  • 针对 candidates 这条增量数据流
  • 给它注册一个最终输出动作
  • 当有候选项进入这一步时,就执行这里面的生成逻辑

这里两个参数的含义:

  • spcSourceProductionContext,主要用来 AddSource
  • symbol:当前这一项候选类的类型符号

换句话说,这块就是“真正生成代码”的地方。

防御式判空

if (symbol is null)
{
    return;
}

严格说,前面 .Where(...) 已经做过空值过滤了,这里再判一次更多是防御式写法。

不是必须,但写上也没问题。

判断目标特性

var hasAttribute = symbol.GetAttributes()
    .Any(a => a.AttributeClass?.Name == "GenerateGreetingAttribute");

这一段的作用是:

  • 读取当前类上的所有特性
  • 判断是否真的带了目标特性 GenerateGreetingAttribute

这里为什么不在前面的 predicate 就直接判断?

因为前面的 predicate 是语法级快速筛选,只知道:

  • 它写了特性

但不知道:

  • 这个特性在语义上究竟是不是 GenerateGreetingAttribute

真正判断特性类型,更适合在拿到 symbol 之后做。

这也是增量生成器很常见的套路:

  • 先便宜地粗筛
  • 再在语义层面精筛

如果不是目标类,就直接跳过

if (!hasAttribute)
{
    return;
}

这很简单:

  • 不是我们要处理的类
  • 就不要生成代码

也就是说,虽然 candidates 是“带特性的类”,但只有其中真正带目标特性的类才会继续往下走。

第 3 步:通常接下来会做什么?

虽然示例后半段代码你已经能看到,但从逻辑上可以直接总结成 3 件事:

1. 读取类的命名空间和名称

通常会有类似代码:

var namespaceName = symbol.ContainingNamespace.IsGlobalNamespace
    ? null
    : symbol.ContainingNamespace.ToDisplayString();

作用是:

  • 让生成代码回到原来的命名空间里
  • 避免用户代码和生成代码命名空间对不上

类名一般直接用:

symbol.Name

2. 拼出源代码字符串

通常会有类似:

var source = $$"""
namespace Demo;

public partial class UserService
{
    public string SayHello()
    {
        return "Hello from generated code";
    }
}
""";

这一步就是把分析结果转成最终要加入编译的 .cs 内容。

为什么通常写成 partial class

因为源生成器不能修改你已有的类,只能再补一份同名 partial 类代码进去。

3. 把源码交给编译器

最后一般是:

spc.AddSource(
    $"{symbol.Name}.Greeting.g.cs",
    SourceText.From(source, Encoding.UTF8));

这一步的意思不是“往磁盘写文件”,而是:

  • 把这段源码加入当前编译
  • 让编译器把它和手写代码一起编译

其中:

  • ${symbol.Name}.Greeting.g.cs 是 hint name,用来标识这份生成文件
  • SourceText.From(...) 是把字符串包装成源码对象

为什么这段代码体现了“增量”?

因为它不是简单的“每次都全量扫描 + 全量生成”,而是把流程拆成了可缓存、可复用的几个阶段:

  • 语法粗筛
  • 语义转换
  • 空值过滤
  • 最终输出

一旦输入没有变化,很多步骤就不需要重新完整执行。

这就是增量生成器相对于传统生成器最有价值的地方:

  • 对大型项目更友好
  • 对 IDE 更友好
  • 对频繁增量编译更友好

再用一句人话总结这段代码

如果把这段增量生成器翻译成最直白的话,它做的事情就是:

  1. 先从所有语法节点里,挑出“带特性的类”
  2. 把这些类转换成真正可分析的类型符号
  3. 再判断它们是不是带了 GenerateGreetingAttribute
  4. 如果是,就为它们生成一份 partial 代码
  5. 最后把生成代码交给编译器一起编译

当你这样理解之后,CreateSyntaxProviderRegisterSourceOutput 就没那么神秘了,它们本质上只是:

  • 一个负责建立输入管道
  • 一个负责注册最终输出

中间再加上筛选和转换而已。

源生成器特别适合哪些场景?

如果你在项目里遇到这些问题,源生成器往往值得考虑。

1. 大量重复模板代码

例如:

  • DTO
  • 配置绑定辅助类
  • API 客户端
  • 事件包装器

2. 运行时反射太多

例如:

  • 序列化
  • DI 自动发现
  • 元数据访问器
  • AOT 场景下的反射替代

3. 规则明确、编译期就能知道

如果规则在编译时就能确定,那就非常适合源生成器。

反过来说,如果必须等到运行时才知道输入,源生成器就不一定适合。

源生成器的几个真实优点

1. 降低运行时反射开销

这是最常见的收益。

特别是序列化、注册、元数据访问等路径。

2. 生成代码同样受编译器保护

它不是字符串模板吐出来就完事,而是最终真的参与编译。

所以:

  • 有类型检查;
  • 有编译错误;
  • 有 IDE 补全;
  • 有重构联动的基础。

3. 更适合 AOT 和裁剪场景

因为很多依赖反射的动态行为,在 AOT / trimming 场景下天然更脆。

源生成器生成的是静态代码,这方面通常更友好。

也别把源生成器想得太万能

它确实很有用,但不是什么问题都应该上。

1. 它不能修改现有代码

你只能新增代码,不能直接把用户手写的方法改掉。

2. 它的调试和维护成本不低

相比普通业务代码,生成器有更强的工具链和 Roslyn 依赖。

如果只是为了省 20 行样板代码,未必划算。

3. 不是所有问题都适合编译期解决

如果信息只有运行时才知道,那源生成器帮不上忙。

4. 生成代码本身也需要设计质量

糟糕的生成器会制造:

  • 难读的 .g.cs
  • 不稳定的文件命名
  • 重复生成
  • IDE 卡顿
  • 增量失效

所以写生成器不是“只会拼字符串”就够了。

调试和查看生成代码

这是写源生成器时几乎必备的技能。

1. 到 obj 目录看生成结果

生成代码通常可以在类似目录里看到:

obj/Debug/net8.0/generated/

不同 SDK 和 IDE 展示方式略有差异,但本质都是中间生成文件。

2. 文件名建议统一 .g.cs

例如:

  • UserService.Greeting.g.cs
  • Person.AutoNotify.g.cs
  • JsonContext.g.cs

这样最容易区分手写代码和生成代码。

3. 生成代码要尽量稳定

所谓稳定,主要指:

  • 同样输入生成同样输出;
  • 文件名可预测;
  • 不要每次编译都乱改格式和顺序。

否则会严重影响调试体验。

写源生成器时最容易踩的坑

1. 忘记让目标类型加 partial

如果你要补的是同一个类或结构体,通常就必须是 partial

2. 只看语法,不看语义

只靠字符串匹配类名、属性名,通常不够稳。

很多时候你最终需要的是 ISymbol,而不是纯语法节点。

3. 重复生成同一个文件

AddSource 的 hint name 要稳定且唯一。

否则就会冲突。

4. 生成器做了太多无谓工作

这是传统生成器最容易出现的问题。

如果项目变大,IDE 性能会明显受影响。

所以再次强调:

  • 新项目优先增量生成器。

5. 把所有逻辑都塞进字符串拼接

业务简单时这样还行;
一旦代码模板复杂,最好抽出:

  • 模型层
  • 渲染层
  • 命名规则
  • 公共帮助方法

否则生成器自己会很快变得不可维护。

一套比较务实的建议

如果你打算在项目里真正使用源生成器,下面这些建议会比较有用:

  • 小型演示先用 ISourceGenerator 理解原理;
  • 真实项目优先 IIncrementalGenerator
  • 通过特性或约定声明“生成意图”;
  • 生成代码尽量走 partial 扩展,而不是奇怪的旁路类型;
  • 生成文件名统一、稳定、可预测;
  • 对高频编译场景,尽量减少全量扫描和重复字符串拼接;
  • 把“编译期适合解决的问题”和“运行时才知道的问题”分清楚。

总结

源生成器的本质,不是“自动造代码这么简单”,而是把一部分原本放在运行时、手工维护、或外部脚本里的工作,前移到编译期来做。

你可以这样理解它:

  • 反射是在运行时读元数据;
  • 表达式树是在运行时拼代码结构;
  • 源生成器是在编译时直接生成新的源码。

在今天的 .NET 项目里,尤其涉及这些场景时,源生成器非常值得掌握:

  • 编译期自动补代码;
  • 减少反射;
  • 提升 AOT 兼容性;
  • 降低样板代码;
  • 做框架或基础设施扩展。

如果你把它当成“Roslyn 里的编译期自动化工具”,通常就不会理解偏。

📰 今日新闻精选:

  • 公积金改革大潮真的来了:时隔 10 年再被写入政府工作报告,10 万亿存量资金有望获得盘活契机
  • 国家超算互联网宣布:龙虾用户每人免费送 1000 万 Tokens;多所高校要求警惕 OpenClaw 安全风险,部分严禁校内使用
  • 我国科学家在银河系边缘发现一对 “双胞胎”,为恒星诞生理论提供了新的观测证据
  • 中国糖尿病治疗领域取得突破:基于干细胞的胰岛再生与移植技术正式进入临床研究阶段
  • 19 名硕士拟录为酿酒工、成装工引发关注,汾酒回应:确为一线岗位,学历高上升空间更大,工资也有差异
  • 网红 20 元 1 米大肉串被曝成本仅 4.5 元,批发商称:成分是纯鸭肉 + 羊油,跟牛羊肉没有关系
  • 华莱士正式宣布退市,上市近十年间仅融资 1000 万元,门店曾突破 2 万家超越肯德基、麦当劳、德克士在华的门店总和
  • 今年 2 月 52 条中日航线取消全部航班,2514 个赴日航班取消,取消率高达 48.5%
  • 福布斯年度亿万富豪榜:共 3428 人上榜,马斯克蝉联首富;中国 539 人上榜,其中张一鸣位列全球第 26 位
  • 外媒:国际能源署建议释放 4 亿桶战略石油储备,规模创历史之最;特朗普宣布将在得州建设美国 50 年来首座新炼油厂
  • 外媒:国际足联主席称特朗普欢迎伊朗到美国参加世界杯;伊朗体育部长称不可能参加美加墨世界杯
  • 韩媒:驻韩美军 6 台 “萨德” 已全部运出基地,将重新部署至中东地区,韩政府回应:反对了但没用
  • 美媒:特朗普称伊朗境内几乎已无可打击的目标,美国对伊朗军事行动即将结束;伊朗称美海军已 "逃离" 伊朗周边海域
  • 外媒:伊朗警告将对美以经济目标采取报复行动,并提醒民众远离银行等潜在目标
  • 美媒:美军内部调查初步认定 “误击” 伊朗学校;美前主持人谴责美军,称 “不值得为这样的国家而战”

📅 今日信息:

  • 公历:2026-03-12 星期四 (植树节) 双鱼座
  • 农历:二〇二六年正月廿四
  • 公历法定节日:植树节
  • 公历纪念日:孙中山逝世纪念日
  • 下一节气:2026-03-20,春分
  • 今年进度:19.45%(已过 71 天,剩余 293 天)

🌟 历史上的今天

  • 1912 年,美国女童子军成立,鼓励女孩们探索户外和领导力,至今影响深远。
  • 1994 年,万维网联盟(W3C)成立,推动了互联网标准的制定,让网页浏览更顺畅。

OpenClaw 每月 Token 开销太高?这 5 个优化帮你省一半

OpenClaw 是一个开源的 AI 助手框架,可以连接大语言模型来构建个人化的 AI 助手。本文针对其运行成本(主要是 Token 消耗)做了一次系统优化,记录 5 个可复用的配置方案,实测综合节省约 50%。

问题背景

如果你也在自建 AI 助手,大概率会遇到同一个问题:Token 消耗比预期高。

这不是因为你用得多,而是因为默认配置往往不是为成本优化设计的——它追求的是开箱即用和功能完整。这两个目标之间有天然的张力:功能完整意味着所有能力默认开启、所有配置走高规格。在你刚上手的时候这是好事,但一旦跑稳了,就有了优化的空间。

一旦你开始认真审视每一次 API 调用的 token 组成,就会发现大量可以省下来的消耗——不是省在功能上,而是省在不必要的重复和冗余上。下面的 5 个优化点就是我从自己的消耗数据中提炼出来的。

我在亚马逊云科技 Bedrock 上跑 OpenClaw 大约三个月后,做了一次系统性的配置优化。花了大约一周时间分析消耗分布、调整配置、对比效果。结果是 Token 消耗降低了约 50%,使用体验基本无感知变化。

为什么默认配置下消耗偏高?因为默认配置的设计目标是"开箱即用"和"功能完整",不是"成本优化"。这两个目标之间是有冲突的——功能完整意味着所有选项默认开启、所有配置默认走高规格。这在你刚上手的时候是好事,但一旦跑稳了,就值得花时间把不必要的消耗降下来。

我把消耗拆解后,发现浪费主要集中在四个方面:所有场景用同一个贵模型、thinking 模式常开、system prompt 太长、心跳太频繁。下面把 5 个核心优化点展开讲。


1. 模型分级:按任务复杂度选模型

这是投入产出比很高的一项优化。

大多数人配置 OpenClaw 时只设一个模型,所有交互都走同一个。但实际上,日常 70-80% 的对话——查个信息、格式化文本、简单问答——用轻量模型就能很好地完成,而且响应延迟更低,体验反而更好。

亚马逊云科技 Bedrock 上的 Nova Lite,价格大约是 Claude Sonnet 的几十分之一。不是几倍的差距,是几十倍。在简单任务上,两者的输出质量差异几乎感知不到。

配置方式:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "amazon-bedrock/amazon.nova-lite-v1:0",
        "fallbacks": ["amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0"]
      }
    }
  }
}

default 是日常模型,处理绝大部分交互;thinking 是高端模型,在需要深度推理、代码生成或复杂分析时使用。

哪些场景适合轻量模型? 日常问答、信息查询、文本格式化和编辑、简单翻译、shell 命令提示、文件内容总结等。这些任务 Nova Lite 处理得很好,响应速度还更快。

哪些场景需要高端模型? 多步推理、复杂代码生成和调试、长文档分析、架构设计讨论等。这些场景切到 Claude Sonnet 才有明显的质量提升。

一个实际体验: 我让两个模型分别处理同一个简单任务——"帮我把这段 YAML 转成 JSON"。结果几乎一样,但 Nova Lite 的响应时间更短。而当我让它们分析一段有 bug 的异步代码时,Sonnet 的分析明显更深入、更准确。这就是模型分级的意义——大部分时候便宜的就够了,偶尔需要好的再上。

效果: 约 30-40% 的费用节省。这个数字取决于你的使用模式——简单交互占比越高,省得越多。


2. Extended Thinking 默认关闭

Claude 的 extended thinking 功能会让模型先生成一段内部推理过程再给出答案,质量提升显著。但代价是 token 消耗平均增加 30-50%。

为什么增加这么多?因为 thinking 模式下,模型会先输出一段"思考链"——可能是几百到几千个 token 的推理过程。这些 token 虽然你不一定看得到(取决于配置),但都在计费。让模型"深度思考"一个查天气的请求,或者"认真推理"怎么排个序,纯属浪费。

{
  "thinking": "off"
}

需要时通过命令临时启用:

/reasoning on

完成后关闭:

/reasoning off

我的判断标准: 如果问题需要"想一想"才能回答好(多条件决策、复杂 debug、架构分析),就开 thinking;如果是随手一问的事情(查个命令用法、格式化个文本),不需要开。

我统计了自己的使用数据,大约只有 15-20% 的请求真正受益于 thinking 模式。其余 80% 的请求开着也是白花钱。

一个常见的误解: 有人觉得"开着 thinking 也没坏处,模型如果觉得不需要深度推理会自动跳过"。实际上不是这样——只要 thinking 模式处于开启状态,模型就会生成推理链,不管任务有多简单。所以"默认开着以防万一"其实是一个代价不小的策略,建议改为"默认关闭,需要时开启"。

与模型分级的协同: 如果日常用的 Nova Lite 本身不支持 extended thinking,那日常对话自然不会产生额外的 thinking token。两项优化叠加效果更好。


3. System Prompt 精简

这是一个容易被忽视但影响持续的优化点。

OpenClaw 的 system prompt 由多个配置文件拼接:AGENTS.md(行为规则)、SOUL.md(人设)、USER.md(用户信息)、TOOLS.md(工具说明)等。关键点在于:这些内容在每次 API 请求中都会完整发送。 不是发一次就缓存了,是每一次请求都带上完整的 system prompt。

如果这些文件加起来有 5000 字(约 2500 tokens),每次请求就先消耗 2500 个 token 在 system prompt 上——还没开始处理你的问题呢,2500 个 token 已经花出去了。每天 50 次交互,一个月就是 1500 次 × 2500 tokens,光 system prompt 就是一笔可观的开销。

优化策略:

  1. 总量控制在 2000 字以内:这是我实测后觉得在"够用"和"省钱"之间比较好的平衡点
  2. 删除冗余描述:模型不需要你反复告诉它"你是一个 AI 助手",它知道
  3. 合并重复指令:多个文件中出现的相同规则只保留一处
  4. 用精炼的短句:把段落压缩成要点,一句话能说清的不写一段
优化前:~5000 字 → ~2500 tokens/次
优化后:~1800 字 → ~900 tokens/次
每次节省:~1600 tokens
月度节省(50 次/天):~240 万 tokens

单次看不明显,但这是一个会在每次请求中复利的优化。而且这些删掉的内容大多是"正确的废话",删了也不影响 AI 的表现。

注意定期审查: system prompt 文件会随着使用逐渐膨胀——你今天加一条规则,下周加个笔记,不知不觉又胖回去了。建议每个月花几分钟看一眼,保持精炼。

具体哪些该留,哪些该删? 留下的应该是真正影响 AI 行为的具体指令——比如"用中文回复""代码块用 markdown""保护用户隐私"。删掉的是通用废话("你是一个有帮助的 AI")、重复出现的规则、以及过度详细的解释。直觉上,如果某句话删了你觉得 AI 的表现不会有任何变化,那就大概率该删。


4. Heartbeat 频率调整

OpenClaw 的 heartbeat 机制让 AI 助手定期主动检查是否有待处理的事务。每次心跳是一次完整的 API 调用,包含完整的 system prompt 和上下文。

这意味着每次心跳的成本跟你手动问一个问题差不多。如果心跳间隔太短,一天累积下来的调用次数相当可观——而且很多时候心跳的结果就是"没事",也就是白花了钱检查了一下发现什么都不用做。

大多数个人用户并不需要那么高频的主动检查。你有事的时候大概率会主动找 AI,而不是等它来找你。

{
  "agents": {
    "defaults": {
      "heartbeat": {
        "every": "45m"
      }
    }
  }
}

为什么选 45 分钟? 这是我在"及时性"和"省钱"之间找到的平衡点。如果你对实时性要求更高(比如依赖心跳做邮件提醒),可以设 30 分钟。如果你几乎不依赖主动通知,甚至可以设 60 分钟或更长。关键是根据自己的实际需求来,而不是用默认值。

同时精简 HEARTBEAT.md,只保留真正需要定期执行的检查项:

<!-- HEARTBEAT.md -->
- 检查待处理的提醒
- 按需添加其他项目,不要贪多

不要在 HEARTBEAT.md 里堆一长串检查清单。每多一项检查,每次心跳的 token 消耗就多一点。只保留真正需要定期自动检查的,其他的手动触发就好。

效果: 心跳从默认高频调到 45 分钟一次,次数减少约 67%。配合内容精简,这部分 token 消耗减少 70% 以上。心跳是 24 小时不停的,这个节省日积月累很可观。


5. 选择合适的计费模式:Bedrock 按需

这一项不是减少 token,而是从计费层面优化。

一些第三方 API 服务有月费或保底消费。对于使用量波动较大的个人开发者——某天用得多、某天几乎不用、周末放假、出差一周——这种固定成本模式不太划算。

亚马逊云科技 Bedrock 的按需模式则是纯按量计费:

  • 用多少付多少,不用不花钱
  • 没有月费或保底:周末休息、假期出游,费用就是零
  • 弹性好:月度波动大也不会浪费
  • 更安全:IAM 角色用临时凭证,不需要在配置文件里放明文 API Key

配合 IAM 角色认证,配置很简洁:

{
  "agents": {
    "defaults": {
      "model": {
        "primary": "amazon-bedrock/amazon.nova-lite-v1:0",
        "fallbacks": ["amazon-bedrock/anthropic.claude-sonnet-4-20250514-v1:0"]
      },
      "heartbeat": {
        "every": "45m"
      }
    }
  }
}

EC2 实例绑定 IAM 角色后,OpenClaw 自动通过角色获取临时凭证,无需在配置文件中存放明文密钥。认证过程对应用透明,由底层 AWS SDK 自动完成。

对个人开发者的好处: 如果你的使用量有明显的波动(工作日用得多、周末少、出差期间不用),按需模式意味着你只为实际使用付费。没有"这个月用不完"或者"超了得加钱"的心理负担。对于还在评估 AI 助手价值的人来说,这种零承诺的模式也更友好——试用成本就是你实际用掉的那些 token,不多不少。


综合效果评估

优化项预估节省比例
模型分级策略30-40%
thinking 模式控制10-15%(与分级叠加后的增量)
system prompt 精简5-10%
heartbeat 频率调优5-10%
Bedrock 按需计费弹性节省,因人而异

前四项叠加后,我的实际 Token 消耗降低了约 50%。第五项的按需计费进一步优化了实际支出。

需要注意:每个人的使用模式不同,以上数字是基于我的场景(以日常对话和简单任务为主,偶尔有复杂编程和分析需求)。如果你的场景以复杂任务为主,模型分级带来的节省会相对少一些;如果你的场景以简单交互为主(比如纯粹当个信息助手用),省的比例可能更高。


操作清单

供快速参考:

  • [ ] 配置模型分级:default 用轻量模型,thinking 用高端模型
  • [ ] 设置 "thinking": "off" 默认关闭 extended thinking
  • [ ] 审查并精简所有 system prompt 文件,总量控制在 2000 字以内
  • [ ] heartbeat 间隔调整到 30-60 分钟,精简 HEARTBEAT.md
  • [ ] 评估 Bedrock 按需模式是否适合你的使用场景

建议从模型分级开始,几分钟改完就能见效。每一项独立,不用一次全做。如果你只做一件事,就做模型分级——投入产出比高得离谱。

推荐顺序: 模型分级 → 关 thinking → 调心跳 → 精简 prompt → 评估 Bedrock。前三步十分钟搞定,效果能覆盖总节省的大部分。


结语

成本优化不是一次性的事情。随着使用习惯的变化、新模型的发布、功能的迭代,定期回顾一下配置和消耗分布是有意义的。

以上 5 个优化点互相独立,可以按需选择实施。核心思路就是:让每一个 token 都花在有价值的地方。 不需要牺牲功能,不需要降低体验,只是把默认配置中的冗余消耗挤掉。

这些思路也不仅限于 OpenClaw。任何基于 LLM 构建的应用——无论是 AI 助手、聊天机器人还是内容生成工具——都面临类似的成本挑战。模型匹配任务、减少不必要的 token 生成、精简固定开销、控制调用频率,这些原则是通用的。

如果你正在为 LLM 应用的运行成本发愁,不妨从分析消耗分布开始,找到你场景下的浪费点,然后对症下药。优化也不是一劳永逸的——每隔两三个月回头看看消耗数据,你可能会发现新的浪费点,也许是 prompt 又膨胀了,也许是有新的更便宜的模型上线了。保持这个习惯,长期受益。

你的 Token 账单长什么样?评论区见 👋

哎 😑 真的承担不起 openclow 的 token 了
各位有什么白嫖 token 的方法吗?急需啊

各位佬们都怎么做的?可否分享一二

目前用的是 vultr+x-ui 自建机场,洛杉矶节点。使用下来感觉速度不够快,不知道是带宽还是距离的原因,大哥们有没有服务器推荐?我买的服务器是最低配 5$/mo ,搬瓦工看了太贵不考虑,最好价格不要超过 10$/mo

需求:速度能够达到 300M 宽带的体验,解锁 GPT 、GEMINI ,最好是亚洲节点(新加坡、日本、韩国)

大家好,我是良许。

在嵌入式开发中,RAM 和 ROM 是我们每天都要打交道的两种存储器。

无论是调试 STM32 单片机时查看内存分配,还是优化程序性能时分析存储结构,深入理解它们的工作原理都至关重要。

今天我们就从底层硬件结构出发,聊聊这两种存储器的本质区别和工作机制。

1. RAM 的结构与工作原理

RAM(Random Access Memory,随机存取存储器)是一种易失性存储器,断电后数据会丢失。

它主要分为 SRAM 和 DRAM 两大类。

1.1 SRAM 的结构原理

SRAM(Static RAM,静态随机存取存储器)使用双稳态触发器来存储数据。

每个存储单元由 6 个晶体管组成,形成一个锁存器电路。

这种结构的优点是只要供电,数据就能一直保持,不需要刷新操作。

SRAM 的基本存储单元包含两个交叉耦合的反相器,它们形成一个正反馈回路。

当存储"1"时,一个反相器输出高电平,另一个输出低电平;存储"0"时则相反。

这种双稳态结构使得数据非常稳定,读写速度也很快。

在 STM32 等单片机中,片上 SRAM 就是这种类型。

比如 STM32F103 系列有 20KB 的 SRAM,它被映射到 0x20000000 地址开始的空间。

当我们定义全局变量或使用 malloc 动态分配内存时,实际上就是在使用这块 SRAM。

// SRAM使用示例
uint8_t global_buffer[1024];  // 全局变量存储在SRAM中
​
void sram_test(void)
{
    // 局部变量也在SRAM中(栈区)
    uint32_t local_var = 0x12345678;
    
    // 动态分配内存在SRAM的堆区
    uint8_t *dynamic_buffer = (uint8_t*)malloc(512);
    
    if(dynamic_buffer != NULL) {
        // 对SRAM进行读写操作
        for(int i = 0; i < 512; i++) {
            dynamic_buffer[i] = i & 0xFF;
        }
        
        free(dynamic_buffer);
    }
}

SRAM 的访问速度非常快,通常只需要几纳秒,而且功耗相对较低(在静态状态下)。

但它的缺点是集成度低,每个 bit 需要 6 个晶体管,因此成本较高,容量也受限。

1.2 DRAM 的结构原理

DRAM(Dynamic RAM,动态随机存取存储器)的结构要简单得多,每个存储单元只需要 1 个晶体管和 1 个电容。

电容充电表示"1",放电表示"0"。

这种结构使得 DRAM 的集成度非常高,可以做到很大的容量。

但 DRAM 有个致命缺点:电容会漏电。

即使不进行读写操作,电容上的电荷也会逐渐流失,导致数据丢失。

因此 DRAM 需要定期刷新,通常每隔几十毫秒就要重新充电一次。

这个刷新操作会消耗额外的功耗,也会占用一定的访问时间。

在嵌入式系统中,如果需要使用大容量内存(比如运行 Linux 系统的 ARM 开发板),通常会外接 DRAM 芯片,比如 DDR、DDR2、DDR3 等。

这些都是 DRAM 的改进版本,通过双倍数据速率等技术提升性能。

// 在Linux应用开发中使用DRAM的示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
​
void dram_usage_example(void)
{
    // 在Linux系统中,malloc分配的内存来自DRAM
    size_t size = 10 * 1024 * 1024;  // 10MB
    char *large_buffer = (char*)malloc(size);
    
    if(large_buffer != NULL) {
        // 初始化内存
        memset(large_buffer, 0xAA, size);
        
        // 进行一些操作
        printf("Allocated %zu bytes in DRAM\n", size);
        
        // 释放内存
        free(large_buffer);
    }
}

1.3 RAM 的读写时序

RAM 的读写操作需要遵循特定的时序。

以 SRAM 为例,读操作的基本流程是:首先将地址信号放到地址总线上,然后拉低片选信号 CS 和读使能信号 OE,经过一定的访问时间后,数据就会出现在数据总线上。

写操作则需要将地址和数据同时准备好,然后拉低片选信号 CS 和写使能信号 WE,保持一定时间后完成写入。

在 STM32 中使用 FSMC(灵活静态存储控制器)外接 SRAM 时,我们需要配置这些时序参数:

void FSMC_SRAM_Init(void)
{
    FSMC_NORSRAMInitTypeDef FSMC_NORSRAMInitStructure;
    FSMC_NORSRAMTimingInitTypeDef ReadWriteTiming;
    
    // 配置读写时序
    ReadWriteTiming.FSMC_AddressSetupTime = 0x00;      // 地址建立时间
    ReadWriteTiming.FSMC_AddressHoldTime = 0x00;       // 地址保持时间
    ReadWriteTiming.FSMC_DataSetupTime = 0x03;         // 数据建立时间
    ReadWriteTiming.FSMC_BusTurnAroundDuration = 0x00; // 总线转换时间
    ReadWriteTiming.FSMC_CLKDivision = 0x00;
    ReadWriteTiming.FSMC_DataLatency = 0x00;
    ReadWriteTiming.FSMC_AccessMode = FSMC_AccessMode_A;
    
    // FSMC配置
    FSMC_NORSRAMInitStructure.FSMC_Bank = FSMC_Bank1_NORSRAM3;
    FSMC_NORSRAMInitStructure.FSMC_DataAddressMux = FSMC_DataAddressMux_Disable;
    FSMC_NORSRAMInitStructure.FSMC_MemoryType = FSMC_MemoryType_SRAM;
    FSMC_NORSRAMInitStructure.FSMC_MemoryDataWidth = FSMC_MemoryDataWidth_16b;
    FSMC_NORSRAMInitStructure.FSMC_BurstAccessMode = FSMC_BurstAccessMode_Disable;
    FSMC_NORSRAMInitStructure.FSMC_WaitSignalPolarity = FSMC_WaitSignalPolarity_Low;
    FSMC_NORSRAMInitStructure.FSMC_WrapMode = FSMC_WrapMode_Disable;
    FSMC_NORSRAMInitStructure.FSMC_WaitSignalActive = FSMC_WaitSignalActive_BeforeWaitState;
    FSMC_NORSRAMInitStructure.FSMC_WriteOperation = FSMC_WriteOperation_Enable;
    FSMC_NORSRAMInitStructure.FSMC_WaitSignal = FSMC_WaitSignal_Disable;
    FSMC_NORSRAMInitStructure.FSMC_ExtendedMode = FSMC_ExtendedMode_Disable;
    FSMC_NORSRAMInitStructure.FSMC_WriteBurst = FSMC_WriteBurst_Disable;
    FSMC_NORSRAMInitStructure.FSMC_ReadWriteTimingStruct = &ReadWriteTiming;
    FSMC_NORSRAMInitStructure.FSMC_WriteTimingStruct = &ReadWriteTiming;
    
    FSMC_NORSRAMInit(&FSMC_NORSRAMInitStructure);
    FSMC_NORSRAMCmd(FSMC_Bank1_NORSRAM3, ENABLE);
}

2. ROM 的结构与工作原理

ROM(Read-Only Memory,只读存储器)是一种非易失性存储器,断电后数据不会丢失。

虽然叫"只读",但现代的 ROM 大多数都可以擦写,只是擦写次数和速度有限制。

2.1 Flash 存储器的结构

在嵌入式系统中,我们最常用的 ROM 其实是 Flash 存储器。

Flash 分为 NOR Flash 和 NAND Flash 两种类型。

STM32 等单片机内部集成的就是 NOR Flash。

NOR Flash 的存储单元是浮栅晶体管。

每个晶体管有两个栅极:控制栅和浮栅。浮栅被绝缘层包围,可以长期保存电荷。

当浮栅中有电荷时,晶体管的阈值电压升高,表示"0";没有电荷时阈值电压较低,表示"1"。

Flash 的编程(写入)操作是通过量子隧穿效应实现的。

在控制栅和漏极之间施加高电压,电子就会穿过绝缘层进入浮栅。

擦除操作则相反,通过施加反向高电压,让电子从浮栅中逸出。

2.2 Flash 的扇区和页

Flash 存储器不能像 RAM 那样随意读写,它有特定的组织结构。

Flash 被分成多个扇区(Sector),每个扇区又分成多个页(Page)。

擦除操作只能以扇区为单位,而编程操作通常以页或字为单位。

以 STM32F103 为例,它的 Flash 被分成多个 1KB 或 2KB 的页。

在编程之前必须先擦除对应的页,擦除后所有 bit 都变成 1(0xFF)。

然后才能进行编程操作,将某些 bit 从 1 变成 0。

如果要将 0 变回 1,必须重新擦除整个页。

// STM32 Flash操作示例
#include "stm32f1xx_hal.h"
​
#define FLASH_USER_START_ADDR   0x08010000  // 用户Flash起始地址
#define FLASH_USER_END_ADDR     0x0801FFFF  // 用户Flash结束地址
​
// 擦除Flash
HAL_StatusTypeDef Flash_Erase(uint32_t start_addr, uint32_t end_addr)
{
    HAL_StatusTypeDef status = HAL_OK;
    FLASH_EraseInitTypeDef EraseInitStruct;
    uint32_t PageError = 0;
    
    // 解锁Flash
    HAL_FLASH_Unlock();
    
    // 配置擦除参数
    EraseInitStruct.TypeErase = FLASH_TYPEERASE_PAGES;
    EraseInitStruct.PageAddress = start_addr;
    EraseInitStruct.NbPages = (end_addr - start_addr) / FLASH_PAGE_SIZE;
    
    // 执行擦除
    status = HAL_FLASHEx_Erase(&EraseInitStruct, &PageError);
    
    // 锁定Flash
    HAL_FLASH_Lock();
    
    return status;
}
​
// 写入Flash
HAL_StatusTypeDef Flash_Write(uint32_t addr, uint32_t *data, uint32_t len)
{
    HAL_StatusTypeDef status = HAL_OK;
    uint32_t i;
    
    // 解锁Flash
    HAL_FLASH_Unlock();
    
    // 按字写入
    for(i = 0; i < len; i++) {
        status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, 
                                   addr + i * 4, 
                                   data[i]);
        if(status != HAL_OK) {
            break;
        }
    }
    
    // 锁定Flash
    HAL_FLASH_Lock();
    
    return status;
}
​
// 读取Flash
void Flash_Read(uint32_t addr, uint32_t *data, uint32_t len)
{
    uint32_t i;
    
    for(i = 0; i < len; i++) {
        data[i] = *(__IO uint32_t*)(addr + i * 4);
    }
}

2.3 Flash 的寿命和磨损均衡

Flash 存储器的擦写次数是有限的,通常 NOR Flash 可以擦写 10 万到 100 万次。

这是因为每次擦写都会对绝缘层造成微小的损伤,累积到一定程度后,浮栅就无法可靠地保持电荷了。

在实际应用中,如果频繁地对同一个扇区进行擦写,会导致该扇区提前损坏。

因此需要使用磨损均衡技术,尽量让各个扇区的擦写次数保持均衡。

比如在存储日志数据时,可以采用循环写入的方式,每次写入不同的扇区。

// 简单的循环写入示例
#define LOG_SECTOR_COUNT    10
#define LOG_SECTOR_SIZE     4096
​
typedef struct {
    uint32_t current_sector;
    uint32_t write_count[LOG_SECTOR_COUNT];
} LogManager_t;
​
LogManager_t log_mgr = {0};
​
void Log_Write(uint8_t *data, uint32_t len)
{
    uint32_t sector_addr = FLASH_USER_START_ADDR + 
                          log_mgr.current_sector * LOG_SECTOR_SIZE;
    
    // 擦除当前扇区
    Flash_Erase(sector_addr, sector_addr + LOG_SECTOR_SIZE - 1);
    
    // 写入数据
    Flash_Write(sector_addr, (uint32_t*)data, len / 4);
    
    // 更新写入计数
    log_mgr.write_count[log_mgr.current_sector]++;
    
    // 切换到下一个扇区
    log_mgr.current_sector = (log_mgr.current_sector + 1) % LOG_SECTOR_COUNT;
}

2.4 EEPROM 的特点

EEPROM(Electrically Erasable Programmable ROM,电可擦除可编程只读存储器)是另一种常见的非易失性存储器。

它的优点是可以按字节擦写,不需要像 Flash 那样以扇区为单位。

但 EEPROM 的容量通常较小,成本也较高。

一些 STM32 芯片没有集成 EEPROM,但可以通过软件模拟的方式,使用 Flash 来实现 EEPROM 的功能。HAL 库提供了相应的接口:

// 使用HAL库的EEPROM模拟功能
#include "stm32f1xx_hal.h"
​
// 写入一个变量到模拟EEPROM
HAL_StatusTypeDef EEPROM_WriteVariable(uint16_t VirtAddress, uint16_t Data)
{
    // HAL库会自动处理Flash的擦除和写入
    return HAL_FLASH_Program(FLASH_TYPEPROGRAM_HALFWORD, 
                            VirtAddress, 
                            Data);
}
​
// 从模拟EEPROM读取变量
uint16_t EEPROM_ReadVariable(uint16_t VirtAddress)
{
    return (*(__IO uint16_t*)VirtAddress);
}

3. RAM 与 ROM 的对比与应用

在嵌入式开发中,RAM 和 ROM 各有其适用场景。

RAM 速度快、可以随机读写,适合存储程序运行时的临时数据,比如变量、堆栈、缓冲区等。

ROM 虽然速度较慢、擦写次数有限,但它能够断电保存数据,适合存储程序代码、常量数据、配置参数等。

在实际项目中,我们需要合理分配这两种存储器的使用。

比如在 STM32 中,程序代码存储在 Flash 中,启动后 CPU 从 Flash 中读取指令执行。

全局变量、局部变量、动态分配的内存都在 SRAM 中。

如果需要保存用户配置或校准数据,可以使用 Flash 的某个扇区或外接 EEPROM。

对于运行 Linux 的嵌入式系统,通常会使用 NOR Flash 存储 bootloader 和内核,使用 NAND Flash 存储文件系统,使用 DRAM 作为运行内存。

这种组合能够在成本、性能和可靠性之间取得良好的平衡。

理解 RAM 和 ROM 的底层结构和工作原理,不仅能帮助我们写出更高效的代码,还能在遇到存储相关问题时快速定位原因。

希望这篇文章能够帮助大家更深入地理解这两种基础而重要的存储器。

更多编程学习资源

128 平方,4 个房间+客餐厅,原打算用 AC+5AP 的方案,后来查了下好像 1 个吸顶 AP (图片标红处)即可全覆盖,麻烦各位帮忙看下什么方案比较合适?

1440X1080/IMG_20260311_224312.png

我买的是 智谱 Max 编码套餐(最高档)

目前我的实际使用情况是:

  • 已使用额度:<5%
  • 剩余额度:≈95%

但系统返回的是:

429 "您的账户已达到速率限制,请您控制请求频率"


先讲讲这个产品逻辑。

如果一个产品是 额度制,那么核心限制应该是:用多少算多少,用完为止。

但现在的实际情况是:额度还剩 **95%**,系统却提示 不可用了


这个体验已经不是“限流”了。

这是 额度和速率设计完全脱节

用户买的是 算力额度只是看的,不是用的,Max 套餐买的不是额度,而是一个 额度余额展示服务, 你根本不会给你用那么多,实属恶心人


Max 编码套餐:

  • 理论额度:很多
  • 实际可用:看缘分
  • 实际速率:429

想安装一个 NPM 版的 Claude Code ,然后就发生了下面的事情:

# tink @ Hackint0sh in /usr/local/lib/node_modules [23:32:52]
$ npm i @anthropic-ai/claude-code

added 3 packages, and removed 1826 packages in 14s

2 packages are looking for funding
  run npm fund for details

忘了打一个-g,又正好在/usr/local/lib/node_modules...

所有的全局包全部被清掉,1826 个,连 npm 、openclaw 都没了。。。

现在一个一个修,好绝望

(1)首先明确性质,这轮炒作的推动力,是由大模型硬件厂商,token 软件大厂,行业相关者为 token 滞销卖不出去而联合推动的一场超级大营销.在这场大营销之前,transformer 大模型遭遇了严重的估值危机,商业回报质疑等,这场大营销也是场自我拯救.

(2)截止 2026 年 3 月,可以非常明确地确认,transformer 大模型没有智力,但确实是一款很人性化的信息管理软件,对解放脑力,提高脑力生产效率意义巨大,是人类脑力助手的里程碑的发明,它的缺点是能源消耗巨大.

(3)尝鲜者必然会付出安全代价和缴纳 token 智商税费用,但只要被骗的人的数量足够多,就能形成行业标准,相关资源会进入其中逐步解决安全问题和 token 费用问题,但最终是否能成功,还有待观察.

(4)openclaw 的概念并不新鲜,早在两年前就有人进行并实际落实,整体上看,这次大推广是正面的,不管中间有多少目瞪口呆的问题,不断犯错才能推动行业往前走

RT ,量化大神 @bdsqlsz 已经在 HF 上传 DeepSeek-V4-INT8 了,看来官宣就在这 48 小时内。

目前流出的 Specs 汇总:

1T 参数 MoE ,激活 32B ;
1M Context ;
支持原生音频(看来要刚 GPT-5.4 的实时语音了);
深度优化昇腾 910C 。


V2EX 不知道如何贴图,相关泄露图大家可以点这查看:
https://deepseekv4.app/zh/news/2026-03-11-leak-deepseek-v4-int8-spotted-huggingface

坐等 DeepSeek 再次震撼!