揭秘前端眼中的Rust!

1a2444a781cef6d19fb0fb020e0a674a.gif

导语 | 本文推选自腾讯云开发者社区-【技思广益 · 腾讯技术人原创集】专栏。该专栏是腾讯云开发者社区为腾讯技术人与广泛开发者打造的分享交流窗口。栏目邀约腾讯技术人分享原创的技术积淀,与广泛开发者互启迪共成长。本文作者是腾讯前端开发工程师于玉龙。

本文主要对rust相关内容进行解读分析,希望本文能对此方面感兴趣的开发者们提供一些经验和帮助。

1f415eec4d14068d8c614598f8ca1906.jpeg

关于Rust

rust是一门强类型的、编译型的、内存安全的编程语言。最早版本的Rust原本是Mozilla基金会的一名叫Graydon Hoare的员工的私人项目,2009年开始,Mozilla开始赞助者们项目的发展,并于2010年,Rust实现了自举——使用Rust构建了Rust的编译器。

Mozilla将Rust应用到构建新一代浏览器排版引擎Servo当中——Servo的CSS引擎在2017年开始,集成到了FireFox当中去。

Rust原本作为一种内存安全的语言,其初衷是代替C++或者C,来构建大型的底层项目,如操作系统、浏览器等,但是因为Mozilla的这一层关系,前端业界也注意到了这门语言,并将它应用在了其他领域,其生态也慢慢繁荣起来。

2dc09a579e2ffa1d1e401b42a3c515de.jpeg

内存安全——Rust的一大杀手锏

众所周知,当下主流的编程语言当中一般分为两类,一类是自动GC的,如Golang、Java、JavaScript等,另一类则是C++和C,用户需要手动管理内存。

大部分语言的内存模型都是大同小异的。

当代码被执行时,一个个变量所对应的值,就被依次入栈,当代码执行完某一个作用域时,变量对应的值也就跟着出栈,栈作为一个先进后出的结构非常符合编程语言的作用域——最外层的作用域先声明、后结束。但是栈无法在中间插入值,因此栈当中只能存储一旦声明、占用空间就不会改变的值,比如int、char,或者是固定长度的数组,而其他值,比如可变长度的数组vector,可变长度的字符串String,是无法被塞进栈当中的。

当编程语言需要一个预先不知道多大的空间时,就会向操作系统申请,操作系统开辟一块空间,并将这一块空间的内存地址——指针返回给程序,于是编程语言就成功将这些数据存到了堆中,并将指针存到栈当中去——因为指针的大小是固定的,32位程序的指针一定是32bit,64位程序的指针也肯定是64bit。

栈中的数据是不需要做内存管理的,随着代码执行,一个变量很容易被判断还有没有用——只要这个变量的作用域结束,那么再也无法读取到这个变量的值,那么这个变量肯定没用了。只需要随着作用域的声明与结束,不断的入栈和出栈就足以管理栈的内存了,不需要程序员操心。

但是堆当中的数据就不行了,因为程序拿到的只是一个内存指针,实际的内存块不在栈当中,无法随着栈自动销毁。程序也不能在栈当中的内存指针变量销毁时,就将指针对应的空间自动清理——因为可能有多个变量保存的指针都指向了同一个内存块,此时清理这个内存块,会导致意料之外的情况。

5e7804580cd0066b371aabeb149e26df.png

基于此,有的程序自带一套非常复杂的GC算法,比如通过引用计数,统计一个内存区块的指针到底保存在多少个变量当中,当引用计数归0时,就代表所有的指向此处的指针都被销毁了,此处内存块就可以被清理。而有的程序则需要手动管理内存空间,任何堆当中开辟的空间,都必须手动清理。

这两种办法各有优劣,前者导致程序必须带一个runtime,runtime当中存放GC算法,导致程序体积变大,而后者,则变得内存不安全,或者说,由于内存管理的责任到了程序员头上,程序员的水平极大程度上影响了代码安全性,忘记回收会导致程序占用的内存越来越大,回收错误会导致删掉不应该删的数据,除此以外还有通过指针修改数据的时候溢出到其他区块导致修改了不应修改的数据等等。

而Rust则采取了一种全新的内存管理方式。这个方式可以简单概括为:程序员和编译器达成某一种约定,程序员必须按照这个约定来写代码,而当程序员按照这个约定来写代码时,那么一个内存区块是否还在被使用,就变得非常清晰,清晰到不需要程序跑起来,就可以在编译阶段知道,那么编译器就可以将内存回收的代码,插入到代码的特定位置,来实现内存回收。换句话说,Rust本质上是通过限制引用的使用,将那些【不好判断某块地址是否还在使用】的情况给规避了,剩余的情况,都是很好判断的情况,简单到不需要专业的程序员,只需要一个编译器,就能很好的判断了。

这样的一大好处是:

  • 不需要GC算法和runtime,本质上还是手动回收,只不过编译器把手动回收的代码插入进去了,程序员不需要自己写而已。

  • 只要编译可以通过,那么就一定是内存安全的。


(一)实现原理

rust的内存安全机制可以说是独创的,它有一套非常简单、便于理解的机制,叫做所有权系统,这里面会涉及到两个核心概念,所有权和借用。


(二)所有权

任何值,包括指针,都要绑定到一个变量,那么,我们就称这个变量拥有这个值的所有权,比如以下代码,变量str就拥有“hello”的所有权。

let str = "hello"

当str所在的作用域结束时,str的值就会被清理,str也不再有效。这个和几乎所有主流语言都是一致的,没有什么问题。也很好理解。

但是注意一下,Rust本身区分了可变长度的字符串和不可变长度的字符串,上文是一个不可变长度的字符串,因为其长度不可变,可以保存在栈当中,于是下面这一段代码可以正确执行,就像其他几乎所有主流语言一样:

let str = "hello world";
let str2 = str;
println!("{}", str);
println!("{}", str2);

但如果我们引入一个保存在堆里、长度可变的字符串,我们再来看看同样的代码:

fn main() {
  let str = String::from("hello world");
  let str2 = str;


  println!("{}", str);
  println!("{}", str2);
}

此时,我们会惊讶地发现,代码报错了。为什么呢?

原因在于,第一段代码当中,str这个变量的值,保存在栈里,str这个变量所拥有的,是hello world这一串字符串本身。所以如果令str2=str,那么相当于又创建了一个str2变量,它也拥有这么一串一模一样的字符串,这里发生的是“内存拷贝”。两个变量各自拥有hello world这一个值的所有权,只不过两者的hello world不是同一个hello world。

而第二段代码当中,我们拿到的str,本质上只是一个指向到某一个内存区块的地址,而这个地址,当我们另str2=str的时候,实际上是将这一个地址的值赋值给str2,如果是在其他语言当中,这么写极大概率是没问题的,但是str和str2会指向同一个内存地址,修改str的时候,str2也变了。但是rust当中,同一个值只能被绑定到一个同一个变量,或者说,某一个变量对这一个值有所有权,就像一个东西同一时间只能属于同一个人一样!当令str2=str的时候str保存的地址值,就不再属于str了,它属于str2,这叫做【所有权转移】。所以str失效了,我们使用一个失效的值,那么自然报错了。

以下这些情况都能导致所有权转移:

上文提到的赋值操作:

let str = String::from("hello world"); let str2=str; //str失去所有权!

将一个值传进另一个作用域,比如函数:

let str=String::from("hello world"); some_func(str); //此时str失效。

这样,我们就可以很简单的发现,对于同一个内存区块地址,它同时只能保存在一个变量里,这个变量如果出了作用域,导致这个变量读取不到了,那么这个内存地址就注定永远无法访问了,那么,这个内存区块,就可以被释放了。这个判断过程非常简单,完全可以放在静态检查阶段让编译器来实现。所以rust可以很简单的实现内存安全。

但,上述的写法是很反人类的,这确实解决了内存安全的问题,但是不好用。比如我需要将str传入一个方法做一些逻辑操作,做完操作之后我还希望我能读取到这个str,比如类似于下面这段代码:

fn main() {
  let mut str1 = String::from("hello world");  // 这里的mut只是标注这个变量是可变的变量,而非常量。


  add_str(mut str1, "!!!");


  println!("{}", str1);
}


fn add_str(str_1: String, str_2: &str) {
  str_1.push_str(str_2);
}

我们希望对str进行操作,后面添加三个感叹号然后打印出来,这段代码肯定是错误的,因为当str传入add_str方法时,就将所有权转移到了add_str方法内的变量str_1上,它不再具备所有权,所以就不能使用了,这种情况其实很常见,单纯的所有权机制让这个问题复杂化了,所以rust还有一个机制来解决下面的问题:【引用和借用】。

借用

虽然一个值只能有一个变量拥有其所有权,但是,就像人可以把自己的东西借给其他人用,借给不同的人用一样,变量也可以把自己拥有的值给借出去,上述代码稍作修改:

fn main() {
  let mut str1 = String::from("hello world");


  add_str(&mut str1, "!!!");


  println!("{}", str1);
}


fn add_str(str_1: &mut String, str_2: &str) {
  str_1.push_str(str_2);
}

add_str传入的不再是mut str,而是&mut str1,这就相当于从mut str1上借了这份数据来使用,但实际上的所有权仍在str1上,内存区块的回收条件,仍然是【str1所在的作用域执行完毕,str1保存的内存地址北出栈而销毁】。

这两种机制,所形成的本质是:对于一块内存的引用计数,变得异常简单,只要这个内存地址对应的变量在堆里,引用计数就是1,否则就是0,只有这两种情况。绝对不存在,多个变量都指向同一个内存地址的情况,这一下子就把引用计数GC算法的复杂度给大幅度降低了。降低到不需要一个复杂的运行时,静态检查阶段就可以得到所有需要GC的时机并进行GC了。

1841d89593cc93d6b70e8a0d24123c94.jpeg

Rust的其他特性

rust作为一个非常年轻的编程语言,它拥有许多新语言常见的特性,在特性方面有点类似于Golang、ts和高版本C++的混合。比如说:

  • 没有继承,只有组合,类似于Golang。继承带来的子类型会带来数学上的不可判定性,即存在一种可能,可以构造出一段包含子类型的代码,无法对它进行类型推倒和类型检查,因为类型不可判定,表现在工程上,那就是编译器在类型推倒时陷入死递归,无法停止。同时,多层的继承也让代码变得难以维护,越来越多的新语言抛弃了继承。

  • 有一个好用的包管理器cargo,可以方便的管理各项依赖。依赖存在项目间的隔离,而非统一放在一块,这一点类似于nodejs,golang也在推进依赖的项目间隔离。项目内安装的依赖被写在cargo.toml当中,并且存在cargo.lock,将依赖锁定在特定版本(几乎和npm一致)。

  • 大量高级的语言特性:模式匹配、没有null但是有Option(任何可能报错、返回空指针的地方,都可以返回一个Option枚举,基于模式匹配来匹配成功和失败两种情况,null不再对开发者暴露)、原生的异步编程支持等等。

45a4f5c41e36a144a0ccf53bc01427c5.jpeg

对前端的影响?

Rust加上上述的一些特性,使得它成为了一个C++的完美替代。目前,前端领域使用Rust有以下两个方向,一个,是使用Rust来打造更高性能的前端工具,另一个是作为WASM的编程语言,编译成可以在浏览器当中跑的WASM模块。

(一)高性能工具

在之前,前端领域如果希望做一个高性能的工具,那么唯一选择就是gyp,使用C++编写代码,通过gyp编译成nodejs可以调用的API,saas-loader等大家耳熟能详的库都是这样实现的。但更多的情况下,前端的大部分工具都是完全不在乎性能,直接用js写的,比如Babel、ESLint、webpack等等,有很大一部分原因在于C++实在不太好入门,光是几十个版本的C++特性,就足够让人花掉大量的时间来学习,学习完之后还要大量的开发经验才可以学会如何更好的做内存管理、避免内存泄露等问题。而Rust不一样,它足够年轻,没有几十个版本的标准、有和npm一样现代的包管理器,还有更关键的,不会内存泄露,这使得即便rust的历史不长,即便C++也能写Nodejs扩展,但前端领域仍然出现了大量的Rust写的高性能工具。比如:

  • swc一个Rust写的,封装出Nodejs API的,功能类似Babel的JS polyfill库,但在Rust加持之下,它的性能可以达到Babel的40倍。

  • Rome也是基于Rust实现,其作者也是是Babel的作者Sebastian。Rome 涵盖了编译、代码检测、格式化、打包、测试框架等工具。它旨在成为处理JavaScript源代码的综合性工具。

  • RSLint,一个Rust写的JS代码lint工具,旨在替代ESLint。

随着前端愈发复杂,我们必定会逐渐追求性能更好的工具链,也许过几年我们就会看到使用swc和Rome正式版的项目跑在生产环境当中了。

(二)WASM

另外,在有了WASM之后,前端也在寻找一个最完美支持WASM的语言,目前来看,也很有可能是Rust。对于WASM来说,带运行时的语言是不可接受的,因为带有运行时的语言,打包成WASM之后,不仅包含了我们自己写的业务代码,同时还有运行时的代码,这里面包含了GC等逻辑,这大大提高了包体积,并不利于用户体验,将带运行时的语言剔除之后,前端能选择的范围便不大了,C++、Rust里面,Rust的优势使得前端界更愿意选择Rust。同时,Rust在这方面,也提供了不错的支持,Rust的官方编译器支持将Rust代码编译成WASM代码,再加上wasm-pack这种开箱即用的工具,使得前端是可以很快的构建wasm模块的。这里做一个简单的演示,下面这一串代码是我从上文提到的swc里面挖出来的:

#![deny(warnings)]
#![allow(clippy::unused_unit)]


// 引用其他的包或者标准库、外部库
use std::sync::Arc;


use anyhow::{Context, Error};
use once_cell::sync::Lazy;
use swc::{
    config::{ErrorFormat, JsMinifyOptions, Options, ParseOptions, SourceMapsConfig},
    try_with_handler, Compiler,
};
use swc_common::{comments::Comments, FileName, FilePathMapping, SourceMap};
use swc_ecmascript::ast::{EsVersion, Program};


// 引入wasm相关的库
use wasm_bindgen::prelude::*;


// 使用wasm_bindgen宏,这里的意思是,下面这个方法编译成wasm之后,方法名是transformSync,
// TS的类型是transformSync
#[wasm_bindgen(
    js_name = "transformSync",
    typescript_type = "transformSync",
    skip_typescript
)]
#[allow(unused_variables)]


// 定义一个可以方法,总共方法由于是pub的,因此可以被外部调用。这个方法的目的是:将高版本JS转义成低版本JS
// 具体的内部逻辑我们完全不去管。
pub fn transform_sync(
    s: &str,
    opts: JsValue,
    experimental_plugin_bytes_resolver: JsValue,
) -> Result<JsValue, JsValue> {
    console_error_panic_hook::set_once();


    let c = compiler();


    #[cfg(feature = "plugin")]
    {
        if experimental_plugin_bytes_resolver.is_object() {
            use js_sys::{Array, Object, Uint8Array};
            use wasm_bindgen::JsCast;


            // TODO: This is probably very inefficient, including each transform
            // deserializes plugin bytes.
            let plugin_bytes_resolver_object: Object = experimental_plugin_bytes_resolver
                .try_into()
                .expect("Resolver should be a js object");


            swc_plugin_runner::cache::init_plugin_module_cache_once();


            let entries = Object::entries(&plugin_bytes_resolver_object);
            for entry in entries.iter() {
                let entry: Array = entry
                    .try_into()
                    .expect("Resolver object missing either key or value");
                let name: String = entry
                    .get(0)
                    .as_string()
                    .expect("Resolver key should be a string");
                let buffer = entry.get(1);


                //https://github.com/rustwasm/wasm-bindgen/issues/2017#issue-573013044
                //We may use https://github.com/cloudflare/serde-wasm-bindgen instead later
                let data = if JsCast::is_instance_of::<Uint8Array>(&buffer) {
                    JsValue::from(Array::from(&buffer))
                } else {
                    buffer
                };


                let bytes: Vec<u8> = data
                    .into_serde()
                    .expect("Could not read byte from plugin resolver");


                // In here we 'inject' externally loaded bytes into the cache, so
                // remaining plugin_runner execution path works as much as
                // similar between embedded runtime.
                swc_plugin_runner::cache::PLUGIN_MODULE_CACHE.store_once(&name, bytes);
            }
        }
    }


    let opts: Options = opts
        .into_serde()
        .context("failed to parse options")
        .map_err(|e| convert_err(e, ErrorFormat::Normal))?;


    let error_format = opts.experimental.error_format.unwrap_or_default();


    try_with_handler(
        c.cm.clone(),
        swc::HandlerOpts {
            ..Default::default()
        },
        |handler| {
            c.run(|| {
                let fm = c.cm.new_source_file(
                    if opts.filename.is_empty() {
                        FileName::Anon
                    } else {
                        FileName::Real(opts.filename.clone().into())
                    },
                    s.into(),
                );
                let out = c
                    .process_js_file(fm, handler, &opts)
                    .context("failed to process input file")?;


                JsValue::from_serde(&out).context("failed to serialize json")
            })
        },
    )
    .map_err(|e| convert_err(e, error_format))
}

这一段Rust代码的特殊之处在于一些方法上加了这样的派生,所谓的派生,指的是我们只要加上这一段代码,编译器就会帮我们实现约定好的逻辑:

#[wasm_bindgen(
    js_name = "transformSync",
    typescript_type = "transformSync",
    skip_typescript
)]

当加上这一段派生之后,编译器就会将下面的函数编译为二进制的WASM函数供JS调用。

我们使用wasm-pack对代码进行编译打包:

wasm-pack build --scope swc -t nodejs --features plugin

拿到以下这些文件:

.
├── binding_core_wasm.d.ts
├── binding_core_wasm.js
├── binding_core_wasm_bg.js
├── binding_core_wasm_bg.wasm
├── binding_core_wasm_bg.wasm.d.ts
└── package.json

然后就可以在JS当中调用了:

// index.js
let settings = {
    jsc: {
        target: "es2016",
        parser: {
            syntax: "ecmascript",
            jsx: true,
            dynamicImport: false,
            numericSeparator: false,
            privateMethod: false,
            functionBind: false,
            exportDefaultFrom: false,
            exportNamespaceFrom: false,
            decorators: false,
            decoratorsBeforeExport: false,
            topLevelAwait: false,
            importMeta: false,
        },
    },
};


let code = `
let a = 1;
let b = {
    c: {
        d: 1
    }


};
console.log(b?.c?.d);


let MyComponent = () => {
    return (<div a={10}>
        <p>Hello World!</p>
    </div>);
}
`;




const wasm = require('./pkg/binding_core_wasm');
console.log(wasm.transformSync(code, settings))

可以看出,只要当下已存在一个Rust库,那么将其转变为WASM是非常简单的,读者也可以去折腾一下Golong、C++的WASM,会发现Rust的整个折腾过程比Golang、C++要简单不少。

e0e273445ef952b48d7199deaa30ca20.jpeg

有没有啥问题?

虽然我上文说了许多Rust的好,但我在学习Rust的时候却有些备受打击,很大的一个原因在于,Rust过于特立独行了。

举一个很简单的例子,在一般的编程语言当中,声明变量和常量,要么有不同的声明方式,如javascript区分let 和const,go区分const和var,要么就是声明出来默认是变量,常量需要额外声明,比如Java声明的变量前面加final就会是常量,而Rust就很特殊,声明出来的默认是常量,变量反而需要额外声明,let a=1得到的是常量,let mut a=1才是变量。

上述提到的,Rust比较特别的点非常多,虽然大部分都只是设计理念不同,没有高下优劣之分,但如此设计确实会给其他语言的开发者带来一部分心智负担。

从我的学习经验来看,Rust本身的学习难度并不低,学习起来实际上未必就比C++简单,社区内也有想学好Rust得先学习C++,不然完全领会不到Rust优雅的说法。想学习Rust的同学,可能需要做好一些心理准备。

 作者简介

f3c5a2deaf66b38f8976fee668bf6672.jpeg

于玉龙

腾讯云开发者社区【技思广益·腾讯技术人原创集】作者

腾讯前端开发工程师,毕业于湘潭大学,目前负责腾讯医疗健康工作室下医疗SaaS产品的前端开发工作,负责组内部分前端工具链开发和维护的工作。

 推荐阅读

如何基于标准化的OpenTelemetry构建APM探针能力

DevOps难以落地之谜,揭开DevOps的神秘面纱!

看完这篇,轻松get限流!

自定义Clang命令,利用LLVM Pass实现对OC函数的静态插桩

779c1c423e5b0457720009afaf7e7957.jpeg

👇点击「阅读原文」,注册成为社区创作者,认识大咖,打造你的技术影响力!

相关推荐

大模型的Agent Skill功能,在LLM HTTP底层交互流中是怎么承载的?

网络文章文本的 skill,昨天我实现了之后也就开始好奇起 Skills 的底层原理了——相比起各种花里胡哨的 agrnt 应用,我一直以来都很对大模型在纯粹的 HTTP 交互层面是如何交互的更感兴趣。Skill 的全部"魔法"都发生在 system prompt 的措辞和 SKILL.md 文件的编写质量上——本质上是一种 prompt engineering + 文件系统的组合。但是各种天花乱坠的 skills,是如何在底层的 HTTP 中,与大模型——这个只会嘴遁的工具交互的呢?

QcloudCommunity的博客 406

万字入门AI Infra:深入理解大模型中的数学与Infra优化

关注腾讯云开发者,一手技术干货提前解锁👇AI Infra科普、入门或者进阶 ~~ 不到 5 万字,带你融会贯通理解大模型的数学、算法、硬件到系统工程。任何问题欢迎沟通交流。可怕,我竟然有点认同这句话。当然大概率是因为姚顺宇太懂AI了,而我则是因为无知者无畏。不过仔细想想,大模型推理中的数学逻辑确实不复杂,很多原理只需要高中数学知识就能看懂。但是为什么像 vLLM 这种推理系统这么复杂呢?本文将拆解大模型中几个核心操作(RMSNorm、Softmax、Causal Mask、Sampling)背后的数学与

QcloudCommunity的博客 1166

平平无奇的源码,竟藏着Agent的核心秘密?

System Prompt 是剧本,Skill 是剧本的扩展包,Agent Loop 是演员按剧本行动。欢迎留言,我们将邀请作者针对性回复你的评论,我们将选取1则优质的评论,送出腾讯云定制文件袋套装。,让 Agent 能主动"巡逻",而不只是被动等待用户消息。OpenClaw 的 System Prompt 分层结构。的"行为底线",任何 Skill 或用户命令都无法覆盖它。精华 2:Skill 的"按需加载"避免 token 浪费。精华 1:System Prompt 是"分层乐高积木"

QcloudCommunity的博客 426

QQ音乐Harness Engineering实践

一个看似简单的需求,背后可能牵动服务调用链、配置、灰度、埋点、错误码、接口契约和历史兼容性。当 AI 开始快速生成大量代码,真正的瓶颈就不再是"写不出来",而是"看不完、想不清、管不住"。在对话式编码阶段,一种典型的工作模式逐渐流行:开发者把需求丢给 AI,扫一眼输出"看起来对"就直接采纳,不审 diff、不追问逻辑、不验证边界。以音乐商业化业务为例,一个需求可能从 TAPD 单开始,经过需求评审、技术方案、服务影响面分析、IDL 契约变更、业务代码实现、测试验证、CR、灰度上线,最后才进入稳定运行。

QcloudCommunity的博客 713

从Prompt、Context到Harness,工程的三次进化与终局之战

同时,强制要求散落各处的决策记录(Slack、邮件、文档)全部迁移至代码仓库,确保 Agent 的唯一信息来源是可信的、版本受控的仓库。更坑爹的是,研究表明,当上下文过长时,模型会出现"中间遗忘(Lost in the Middle)"现象:它对开头和结尾的内容记忆较好,对中间大段内容的关注度大幅下降。这篇文章,我想带你完整走一遍这三次进化的逻辑:它们分别解决了什么问题,它们之间是什么关系,它们的边界在哪里,以及:当三者融合,AI 工程师的终极形态究竟是什么?大语言模型的本质,就是这位金鱼助理。

QcloudCommunity的博客 718

让Skill自己训练自己:8阶段Loop、3层评测、5维AND门控,从此实现自进化

所以每一轮改动都是有据可查的,不是「感觉这里可以优化」,而是「Case 41 挂了,因为 agent 把离职问题路由到了邮箱分类,但正确答案在通讯录,所以我改 root_index.md 加个易混淆提示」。但让我最后决定把它记录下来的,不是这的个「吸收」过程,而是它真的在替我探索我不想费脑力触达的边界,这看起来更像是一个机器智能应该有的样子。不准跨层,每轮只改一层的东西。但我经过这 19 轮之后觉得,更准确的描述是互补——你在明处看着,AI 在暗处替你试错,你看不见的那一半就是 AI 能贡献的地方。

QcloudCommunity的博客 1127

只加两行代码,为什么要两天?一文深度理解业务系统的复杂性

关注腾讯云开发者,一手技术干货提前解锁👇业务系统复杂性一直是令开发者头痛的问题。复杂的不是增加一个需求需要耗费多少时间,而是在增加一个需求后带来的蝴蝶效应:其它功能会不会受到影响、要如何去找到这些影响,最终如何实现系统正常运行......功能之间隐秘增加的耦合、不可避免的代码腐化在导致业务复杂性增加。大家都在说的软件开发提效到底在提什么?程序员日常工作中应该如何提升开发效率?敏捷开发、瀑布流式开发孰是孰非?欢迎阅读。业务背景与目标我依然非常清晰地记得某个时候,我所在团队 Leader 曾讲过一段话:“我担

QcloudCommunity的博客 712

从零设计生产级 Multi-Agent Harness:架构、评估、记忆、成本与 MCP 工具接入全拆解

在 AI Agent 领域,Multi-Agent Harness 指的是把多个 Agent 的能力、工具、状态、通信、编排、监控统一收束在一个运行时之内的框架。第三,Agent 路由。一个输入框,一个大模型,几个工具,一段写得很漂亮的 System Prompt,再配上一个炫酷的前端,看起来就像一个能干活的“数字员工”。Multi-Agent 完全不一样——它有计划、有中间步骤、有工具调用、有 Agent 协作、有重试、有反思、有最终合成。增加 Budget、权限、重试、压缩、轨迹评估、审计、回归测试。

QcloudCommunity的博客 710

拆完Hermes源码,我发现Agent的“自我进化“根本不需要训练模型

两个月 5.2 万 Star,Hermes Agent 用一个"LLM 审判官"机制实现了 Agent 的自我进化——不改模型权重,只改"怎么用模型"的策略。OpenClaw(社区昵称"龙虾"🦞)是 2025-2026 年最火的开源 Agent 框架,30 万+ Star,插件市场 ClawHub 里有数千个插件,能接入 Telegram、WhatsApp、Slack、飞书、企业微信——几乎是一个"个人 AI 操作系统"。Skill 是英语写的,Memory 是英语写的,审查 Prompt 也是英语写的。

QcloudCommunity的博客 909

AI Infra 其实没有多少新东西

传统 Infra 追求横向扩展,而 AI Infra 呈现 “AI 大型机”特性,是因为传统后台服务的可以容忍毫秒级延迟,但 AI 集群不行,GPU 的算力是 CPU 的数百倍,微秒级的延时等待也会造成很大的算力损耗,需要硬件的高度集成。在可预见的1-3年的未来,这样的专用硬件+网络的集中式架构很难发生比较大的改变。与之相似,模型推理的批处理就是将多个输入样本打包(batch),将原本串行的 N 次轻量的推理计算,合并为 1 次重量的计算,实现单位时间内处理更多的请求,提高了 GPU 利用率。

QcloudCommunity的博客 824

程序员越早想通这些越好

永远没有正确架构,你永远无法偿还完所有的技术债务,如同你永远不会设计出完美的界面,请避开完美主义的陷阱,永远没有完美的代码,能够满足当前需求,又为未来不管是自己还是他人留有扩展空间,就很好了。我们努力的方向就是将现实世界映射到有限的数据结构中,这就需要用户自定义类型,如果你的代码中包含类似 trader portfolio 这样的概念,你就可以使用他们的名称来建模,而不是使用基本数据类型复杂嵌套表述你的目的。从另一个角度看,代码里的注释都是补丁,是碎片化的,局部的注释很难推导模块整体的顺序及耦合关系。

QcloudCommunity的博客 465

RAG已死?不,是Grep回归了!

这样一来可以直接复用已有的计算缓存,只为新增的增量部分付全价,前面累积的大头按约 1/10 的缓存价计费。所以实际的工具组合方式是灵活的:Grep(默认模式)→ Read 是最常见的路径,但 Grep(content 模式)可以独立使用,LLM 也可以直接调 Read(如果已经知道文件路径),或者一次同时发起多个 Grep 并行搜索。它从零开始构建自己的对话历史,不继承主对话的消息,这意味着它搜索过程中产生的大量 grep 结果、代码片段都留在自己的 context 里,主对话只收到一段总结性的文本结论。

QcloudCommunity的博客 576

深入浅出Harness Engineerring之核心模式与理念

在智能体生命周期的关键节点(如会话开始、工具调用后)自动触发预设动作(如代码格式化),由系统确保关键流程被执行,不依赖可能被模型遗忘的指令。简单讲就是利用向量存储这个技术,来实现模糊检索,原理是:即使字面不同,但语义相近的文本,其向量在数学空间中的位置也很接近。根据命令类型、参数和影响,自动评估其风险等级(安全、有风险、危险),并采取自动执行、请求确认或直接拦截等不同策略。关键特性是可隔离、可重建、可扩展。不是一次判断就定终身,允许你改变、允许情况复杂,通过不断观察、思考、调整,越来越懂真实的你。

QcloudCommunity的博客 623

Harness Engineering实践心得:如何高效驾驭AI?

但如果这些经验被拆成一条条 Rule、一个个 Skill、一项项 Script 检查——"XAML 不能有中文"、"编译要用这个路径的 MSBuild"、"SVN 命令必须带认证参数"——AI 就能在秒级时间里"吸收"十年的踩坑经验。然后可以看到下方的图,我的PM会很顺利的调度其他agent分别负责不同的事项,遇到问题的时候也能准确的找到合适的agent。MCP 本质上是在扩展 AI 的"感知范围"——Rule 管"该怎么做",Skill 管"怎么操作",MCP 管"能看到什么、能碰到什么"。

QcloudCommunity的博客 841

从第一性原理思考 Agentic Engineering

需要注意的是,这一结论是有条件的:如果 AI 缺乏领域知识,过早介入反而可能引入 1.2 节所述的"似是而非"损耗——因此上下文供给(实践 1)是 AI 全链条参与的前提,而非结果。通过一个专门的自迭代 Skill 实现——当 AI 在协作过程中识别到自己被纠正(用户否定了 AI 的输出并给出了正确方向),它自动评估这个错误的根因,搜索现有的 Rules 和 Skills 中是否已有针对该类问题的处理规则。事实上,越是复杂的项目,工程师的认知负担越重(公理 3),AI 分担认知负担的潜在边际价值也越大。

QcloudCommunity的博客 757

万字干货!Harness Engineering如何工程化落地?

关注腾讯云开发者,一手技术干货提前解锁👇Harness Engineering 的概念已经火了有一阵了,全网很多文章基本都是在讲理念,讲为什么今天做 AI 开发,不能只靠一段提示词,也不能把模型当一个“更聪明的代码补全”。但与此同时,一个问题也在持续地发酵:这个问题特别重要。因为 Harness 这个词听上去很大,很像一个抽象的方法论,但它如果不能落到工程、目录、文档、脚本、工作流里,那它最后就只是一句漂亮的话。事实上,不同项目落地出来的 Harness,表面上可能完全不一样。有的项目是 CI 很强,有的

QcloudCommunity的博客 1169

全方位对比:Hermes VS OpenClaw

多账号矩阵运营 如果你的业务涉及同时管理多个社交媒体账号、多个平台的内容分发、多渠道的客户互动——OpenClaw是目前市场上的"无可替代的底座"。一个有专业背景的个人,现在可以快速构建一个垂直领域的AI助手:比如"财务分析Hermes"、"法律咨询Hermes"、"技术架构Hermes",直接通过微信提供服务,积累自己的专业IP。跨平台生态融合 如果你的工作流涉及飞书、Slack、Discord等多个平台的深度集成,需要这些平台之间的数据流转和联动——OpenClaw的生态支持是强大的。

QcloudCommunity的博客 1217

一文讲透:Harness Engineering即控制论!

而研发模式的升级我认为将会是一个飞跃的过程,之所以现在我们仍然需要需要参与编码,一个非常大的原因就是缺乏传感器和基建,仍然需要人来串流程,比如需要人来验证UI是否正确、需要人来告诉AI代码不符合代码规范,因此在有些场景我们仍然感觉 AI 完成的很吃力,但这不是 AI 能力不行,而是我们并没有为 AI 提供友好的环境……控制论中的突变理论介绍了矫枉过正和飞跃现象之间的矛盾,如果质变中经历的中间过渡态是不稳定的,那么他就是一个飞跃的过程,如果中间过渡态是稳定的,那么他就是一个渐变的过程。但是这些阶段要解决的。

QcloudCommunity的博客 1203

神级开源插件!一句话让QClaw变3D小镇,Agent工作可视化,赛博云监工

这个架构是翻了几次车才想通的—原有AI的实现机制是用 setTimeout 猜动画时长导致了幽灵 NPC、工位撞车、越改越崩,后面我停下来换了思路,推翻原有的架构,按照“意图+回传”的架构进行重构,之前的问题一次性全部解决。15 天,我和 AI 吵了 185 次架, 4.5 万行代码,开发了一款 QClaw / OpenClaw 的UGC游戏插件,这可能是人类跟 Agents 交互的新范式,插件已完全开源,有需要的欢迎自取~我天天用,用着用着我就在思考,Agent 产品现在的交互体验,基本都一个样。

QcloudCommunity的博客 1196

一文搞懂Hermes:新顶流Agent如何从经验中自我进化

Voyager 是一个能在 Minecraft 中自主探索、学习技能、无限积累能力的 AI Agent,它提出了 "Skill Library"(技能库)的概念——Agent 将成功的行为序列编码为可复用的技能函数,存储在一个持续增长的代码库中。如果 Skill 的名称和描述不够精准,或者用户的任务描述与 Skill 的触发条件有语义差距,Agent 可能会错过相关的 Skill。如果进程在写入过程中崩溃,目标文件要么是旧内容(还没被替换),要么是新内容(替换已完成),绝不会出现写了一半的损坏文件。

QcloudCommunity的博客 2634

自掏腰包一万元,拥抱AI这一年,我的工具、实践和思考

其实也很好理解,过往的每个领域(比如前端、后端、设计、测试等等)都主要是这个领域的从业者在为它添砖加瓦,但是大模型时代的 AI 应用领域却几乎是所有使用 AI 的人类一起在迭代,无分领域,无分文理,无分老少。因为每个时期的 AI 都有着不同的特点,很多时候当我费尽心思去钻研,自以为有点弄明白 AI 应该怎么玩的时候,基座模型和行业范式常常就会迎来颠覆式的变革,很多过往的经验都会倾覆,然后只能从头再来。过去一年多,被热议的 AI 范式大概是沿着四个阶段递进演化,每一个阶段其实都在解决前一个阶段的核心问题。

QcloudCommunity的博客 627

办公Agent的CI/CD时刻到来了

核心观点是:企业给员工配了一堆 AI 工具,试点跑了几百个,但真正拖慢 AI 转型速度的瓶颈不在模型能力,在"最后一公里":AI 输出和实际工作流之间的那道鸿沟。代码写完,打个 tar 包,开 FTP 传到服务器上,SSH 进去解压,改配置文件,重启服务,刷一下页面祈祷别 500。你复制,切窗口,粘贴到腾讯文档里,发现 Markdown 格式全乱了,手动调标题层级,代码块重新标记语言类型,表格散架了重排一遍。模型是越来越聪明了,但"生成"到"落地"之间那段路,还是你自己在走,一步一步,手动搬运。

QcloudCommunity的博客 608

4亿token买来5个教训:让6个AI Agent连写4天代码发生了什么?

Watchdog 检测到崩溃 → 启动诊断 Agent 分析原因 → 诊断 Agent 跑满 20 轮没搞定 → Watchdog 判定“已修复”,重启系统 → 重启的系统因为诊断 Agent 留下的环境变量被继承,连续 3 次拒绝启动 → 触发连续失败保护退出 → Watchdog 第 3 次巡检,发现进程和记录文件都没了,尝试手动重启,自己也停了。启动前我翻了论文、做了架构设计、写了监控+修复异常的脚本。这个发现让我的情绪从沮丧变成了兴奋——不是那种"终于修好了"的释然,是"我找到根因了"的快感。

QcloudCommunity的博客 584

赛博斗蛐蛐:9大模型决战三国志,天命在谁?

我猜是这样的,当Gemini面临决策的时候,一系列关键词比如 守城,劣势,粮草,三国等等,在语义层面的关联性中LLM发现了竖壁清野这个关键词概率非常高,所以以此为策略,然后再根据这个策略完美执行操作。MiMo 的外交是三方中最精明的。Kimi作为国产模型,也同样颠覆了我对他的看法,最后的比赛他甚至一度是优势者,存在很多夺冠的可能性,要知道他的对手可是claude和gemini,而他经常出兵慢,回过头来想想或许正是他的策略,毕竟中后期他的优势都来自于稳健的发展和较少的战斗频率(和claude比).

QcloudCommunity的博客 874

Claude Code是怎么知道你在骂他的?这 12 条发现值得关注

确实存在——5,500+ 行,集成了消息队列、工具装配、MCP 编排、会话状态、权限、分析、文件持久化和优雅关闭,基本上一个文件干了所有事。对抗虚假声明——"不要在输出显示失败时声称所有测试通过,不要压缩或简化失败的检查来制造绿色结果"。完整的 KAIROS 是一个持续驻留的守护进程,能感知你在不在电脑前、主动寻找有用的工作做,更像是住在终端里的全职同事。源码里藏着一个叫 "Buddy" 的电子宠物系统,完整度令人惊讶——这不是一个 TODO,而是一个从数值系统到 ASCII 动画都实现了的完整功能。

QcloudCommunity的博客 701

逆向深扒Claude Code源码,我发现了什么!?

Anthropic 内部用户获得更严格的质量控制 prompt、更好的验证机制(独立 Verification Agent)、以及针对 model 弱点的专门对策(如 Capybara v8 的 29-30% 错误声明率对策)。这不是代码混乱的标志——它是整个 Agent Loop 的心脏,所有的流式处理、工具调度、上下文压缩、子代理管理都在这个文件中协调。这种"胖核心"设计是刻意的:保持核心循环的原子性,避免分散到多个模块导致的协调复杂度。核心价值:Fork 代理的工具输出不会回到主代理的上下文窗口。

QcloudCommunity的博客 892

一文讲透如何构建Harness——六大组件全解析

本文将从"裸模型的四个硬伤"出发,逐一拆解 Harness 的六大组件——文件系统、Bash + 沙箱、记忆(AGENTS.md)、Web Search + MCP、上下文工程、编排 + Hooks——带你看清 AI Agent 真正的竞争壁垒,不在模型层,而在 Harness 层。如果你用过 Agent 做复杂任务,你一定有过这样的体验:一开始 Agent 的表现很好,但随着对话变长,它开始"变笨"——遗忘之前的约定,重复之前犯过的错误,甚至对同一个问题给出前后矛盾的回答。对于简单的问答场景,这无所谓。

QcloudCommunity的博客 761

Harness Engineering 来了,SDD 还有意义吗?

他们的解法是把「品味」编码进规则,然后用一个定期运行的「doc-gardening」Agent 自动扫描漂移、发起修复 PR——「技术债务就像一笔高息贷款,不断地以小额贷款的方式偿还,总比让债务不断累积要好」。代码仓库本地的、已版本化的工件,才是 Agent 所能看到的全部。在 Vibe Coding 模式下(即不做规划、随意向 AI 提需求的开发方式),大量设计决策、跨服务约定、「为什么不选另一个方案」的讨论,全部停留在一次性的 AI 对话里——下一个人,或者下一次 AI 会话,完全无从获取。

QcloudCommunity的博客 419

零废话!一文讲透从0构建AI Agent

每个工具都要写一套"定义→解析→执行"的胶水代码。上下文管理就是在"记住足够多"和"别超标"之间找平衡,常见策略包括滑动窗口、摘要压缩、RAG 检索增强。上下文窗口是 Agent 的"工作记忆",但它有硬性限制。LLM 负责"动脑"决策,代码负责"动手"执行。这是从"聊天机器人"进化到"Agent"的关键一步。每次调 API 都把完整对话历史发过去,LLM 本身是无状态的,"记忆"完全靠客户端的数组模拟。这样 Agent 一开始就知道自己"在哪里"、"手头有什么",不需要先花一轮工具调用去探索环境。

QcloudCommunity的博客 711

距离AI把小编我裁掉还有72小时

这是一个基于 OpenClaw 开源生态打造的本地化 AI Agent 助手,已于上周开启全量公测,无需复杂配置,三步即可上手,自带安全防护,支持微信直连绑定,内置多款优质国产大模型,现阶段也是完全免费使用,基本上前面提到过的痛点都可以靠它解决。对了,这里特别强调一点,上面的截图让产品同事汗流浃背,再三找我确认 QClaw 思考过程中优美的“草”字说法,不会是默认出现的吧?AI 好,AI 得学,但更重要的是,在你的工作和生活里,哪些东西是能结构化地被 AI 所接入的,不要死守着那些脏活累活不放。

QcloudCommunity的博客 673
上一篇: 看完这篇,轻松get限流!
下一篇: 大数据架构系列:如何理解湖仓一体?
腾讯云开发者
腾讯云开发者 企业官方账号 企业官方账号
博客等级 码龄10年 6291粉丝 1962原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值