Skip to main content

78 posts tagged with "Rust"

View All Tags

摘要:从 Python 的 Pydantic 到 Rust 的 Serde+Schemars,本文探讨如何在生产环境中,利用 Rust 的类型系统强制约束 LLM 的输出,实现从“概率性文本”到“确定性数据”的完美转换。


一、 背景:智能体开发的“最后一公里”危机

过去一年是 AI Agent(智能体)概念的爆发期。Coze、Dify 等低代码平台的出现,极大地降低了 Demo 的构建门槛,让非开发者也能通过拖拽工作流(Workflow)快速验证想法。

然而,当我们将视角从 Demo 演示 转向 企业级生产环境 时,立刻会撞上“天花板”:

  1. 黑盒集成的痛苦:低代码平台难以无缝嵌入现有的复杂业务后端(如遗留的 Java/Go 微服务)。
  2. 复杂逻辑的泥潭:可视化的连线在处理多重循环、状态回滚、分布式事务时,维护成本呈指数级上升。
  3. 性能与成本的瓶颈:这是最致命的。生产环境对 延迟 (Latency)吞吐量 (Throughput) 极其敏感,Python 的解释器开销在高并发场景下往往显得力不从心。

二、 为什么是 Rust?超越 Python 的工程化极限

Python 无疑是 AI 领域的“第一公民”。其生态中的 Pydantic 堪称神器,集类型定义、Schema 生成、数据验证于一身。

除了复杂智能体需要特定的编排方式,其余基本都算是后端开发的范畴,在高性能服务中Python 始终是不占太多优势的。原因不仅在于性能,更在于安全性

  • 编译期这一关:Python 的 Type Hint 只是提示,而 Rust 的类型系统是法律。
  • 零成本抽象:Rust 允许我们在不牺牲性能的前提下,构建极高层级的抽象。
  • 并发模型:Tokio 的异步运行时能够以极低的资源消耗处理海量 I/O,非常适合高频调用 LLM API 的网关层。

我们要在 Rust 中复刻甚至超越 Pydantic 的体验:不依赖臃肿的框架(如 LangChain),通过原生组合拳实现极致可控的结构化输出。

三、 核心挑战:非结构化智能 vs 结构化系统

智能体在内部思考时可以是发散的(Chain of Thought),但在对外交互时必须是严谨的。 有了规范的输出后续的处理中才不至于出错,或者说出错了不知道从哪里找,有了这种不稳定性和不确定性, 那么智能体服务就会很难维护。

智能体前面的节点的确定输出是后面智能体节点输出正确结果的保障。

JSON object vs JSON Schema

目前主流的 JSON 输出控制有两种模式:

  1. JSON Object 模式:保证输出是合法的 JSON,但字段结构需要通过 Prompt 约束。
  2. JSON Schema 模式:严格遵循 Schema,但目前仅部分模型支持(如 qwen-plus),且配置较为繁琐。

本文采用一种更通用、兼容性更强的“组合拳”策略: 利用 Rust 的类型系统自动生成 Schema,注入到 System Prompt 中,并配合 json_object 模式。这既保证了灵活性,又能适配绝大多数支持 JSON Mode 的模型(OpenAI, Qwen, DeepSeek 等)。

技术栈映射表

核心能力Python 生态 (Reference)Rust 生产级方案优势
数据定义class Model(BaseModel)**struct + serde**内存布局紧凑,无运行时开销
Schema 生成.model_json_schema()schemars编译期自动派生,SSOT (Single Source of Truth)
网络层requests / httpxreqwest极致的异步性能
解析验证.model_validate_json()serde_json严格的反序列化,失败即 panic/err,绝不处理脏数据

💡 兼容性提示 虽然 OpenAI, Claude, Gemini 以及 Qwen-plus 等模型明确支持严格的 Schema 模式,但许多优秀的开源模型或 API(如 DeepSeek, GLM, MiniMax, MoonShot)目前主要支持 json_object 模式。 本方案通过 Prompt 注入 Schema + JSON Mode 兜底,最大程度地兼容了这两种生态。

四、 实战代码:构建类型安全的提取器

假设我们需要一个智能体,从非结构化的技术需求文档中提取元数据,供下游的大数据 Pipeline(如 Spark/Flink)使用。

1. 极简依赖配置 (Cargo.toml)

拒绝过度封装,回归 HTTP 本质。

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
schemars = { version = "0.8", features = ["derive"] } # 核心:将 Struct 转换为 JSON Schema
reqwest = { version = "0.11", features = ["json"] }

2. 定义“真理之源” (Single Source of Truth)

在 Rust 中,struct 就是契约。注意 #[schemars(description = "...")],这不仅仅是注释,它会被编译成 JSON Schema 注入给 LLM,实际上起到了 Prompt Engineering 的作用。

use serde::{Deserialize, Serialize};
use schemars::{schema_for, JsonSchema};

/// 数据流水线配置
/// Derive 宏解析:
/// - Serialize/Deserialize: 处理网络传输
/// - JsonSchema: 为 LLM 生成"说明书"
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct DataPipelineConfig {
#[schemars(description = "数据处理任务的唯一标识名称,建议使用蛇形命名法")]
job_name: String,

#[schemars(description = "任务优先级,1 为最高,10 为最低")]
priority: u8,

#[schemars(description = "数据源列表 (例如: s3://bucket/data)")]
sources: Vec<String>,

#[schemars(description = "集群资源配置详情")]
cluster_config: ClusterConfig,
}

#[derive(Debug, Serialize, Deserialize, JsonSchema)]
struct ClusterConfig {
#[schemars(description = "所需的 Worker 节点数量")]
worker_count: u32,

#[schemars(description = "每个节点的内存大小 (GB)")]
memory_gb: u32,
}

3. 核心逻辑:Schema 注入与运行时验证

use serde_json::json;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
// === 1. 动态生成 Schema ===
// 这里的精髓在于:你只需要修改 Rust struct,Prompt 就会自动更新。
// 永远不会出现“代码改了,Prompt 没改”导致的幻觉问题。
let schema = schema_for!(DataPipelineConfig);
let schema_string = serde_json::to_string(&schema)?;

// === 2. 构建 System Prompt ===
// 我们将 Schema 作为“上下文”的一部分注入,明确要求 LLM 依此行事。
let system_prompt = format!(
"You are a system architect. \
Analyze the user requirement and output a strict JSON configuration. \
Do NOT include markdown formatting or explanations. \
The JSON must strictly adhere to this schema: {}",
schema_string
);

// === 3. 模拟用户模糊输入 ===
let user_input = "I need a high priority job named 'LogAnalyzer' to process logs from s3://logs/app-a. \
We need a beefy cluster with 10 nodes and 64GB memory each.";

// === 4. 发起 HTTP 请求 ===
let client = reqwest::Client::new();
let api_key = std::env::var("OPENAI_API_KEY").expect("API Key not set");

let response = client
.post("https://api.openai.com/v1/chat/completions") // 或兼容 OpenAI 协议的 URL
.header("Authorization", format!("Bearer {}", api_key))
.json(&json!({
"model": "gpt-4-turbo", // 或 qwen-plus 等
"messages": [
{ "role": "system", "content": system_prompt },
{ "role": "user", "content": user_input }
],
// 关键点:启用 JSON 模式,强制模型输出合法的 JSON 语法
"response_format": { "type": "json_object" },
"temperature": 0.1
}))
.send()
.await?;

// === 5. 解析与验证 (The Moment of Truth) ===
let response_body: serde_json::Value = response.json().await?;

// 提取 content
let content = response_body["choices"][0]["message"]["content"]
.as_str()
.ok_or("No content in response")?;

// Serde 自动验证:
// 如果 LLM 返回的字段类型错误(比如 priority 给了 "high" 字符串而不是数字),
// 这里会直接抛出 Err,保护下游系统不被污染。
let config: DataPipelineConfig = serde_json::from_str(content)?;

println!("=== ✅ 配置解析成功 ===");
println!("Job Name: {}", config.job_name);
println!("Workers: {}", config.cluster_config.worker_count);
println!("Memory: {} GB/Node", config.cluster_config.memory_gb);

// 此时 config 已是内存中安全的 Rust 对象,可直接传递给 Spark/Flink SDK
Ok(())
}

五、 为什么这对生产环境至关重要?

  1. 单源真理 (Single Source of Truth): 代码即文档,文档即 Prompt。修改一处 struct 定义,你的业务逻辑、API 文档、以及发给 LLM 的指令全部自动同步。
  2. 防御性编程: 在 Python 中,类型转换往往是隐式的。而在 Rust 中,u8 就是 u8。如果 LLM 产生了幻觉返回了 256(超出 u8 范围),Serde 会在数据进入业务逻辑层之前立即拦截。这种**“Fail Fast”**机制对于金融交易或大数据处理等零容错场景是无价的。
  3. 可预测的性能: 使用 reqwest + serde,你完全掌控了内存分配和网络行为。没有 Python GIL 的锁竞争,没有复杂的动态类型推断开销。

六、 总结

随着 AI 应用逐渐从“玩具”走向“工业品”,我们对系统的确定性要求越来越高。

虽然 Python 生态在实验阶段不可替代,但在追求极致性能、类型安全和系统稳定性的后端架构中,Rust + Serde + Schemars 方案展示了一条更硬核、更可靠的道路。这不仅仅是编程语言的选择,更是一种对数据严谨性绝不妥协的工程态度。

七、 后续

后续还会一直在Github上更新各个模型的适配方案。

链接

GitHub: https://github.com/yuxuetr/rust-llm-output-json-format

鱼雪

如果你也是一名曾在 Rust 的强类型和所有权机制中找到安全感的开发者,或者是习惯了 Data Class + Function 这种数据与行为分离模式的后端工程师,当你回到 Python 的动态世界时,可能会有一种“裸奔”的不安感。

字典(Dict)满天飞,类型提示(Type Hint)形同虚设,运行时的 KeyError 像一颗颗地雷。

在我的技术栈中,Pydantic 早就不再仅仅是一个简单的“数据验证库”。它是 Python 通往“强类型”和“结构化思维”的唯一桥梁,是它让 Python 拥有了类似 Rust Struct 的严谨。

要把 Pydantic 用好,核心是将它视为 系统边界的守门人。以下是如何在项目中“重度”且“优雅”地使用 Pydantic 的 5 个层级。


层级 1:消灭字典传参 (The Death of Dict)

在传统的 Python 代码中,字典是数据传递的通用货币。这其实是维护的噩梦:你永远不知道 data['user_id'] 到底是 str 还是 int,甚至不知道这个 Key 是否存在。这被称为“Stringly Typed”编程。

做法:强制规定,系统边界以内,严禁裸奔的字典。

所有进入函数的复杂数据,必须在入口处转换为 Pydantic Model。

❌ 修改前 (裸奔的字典)

def process_data(data: dict):
# 没有任何 IDE 提示,如果不看代码实现,不知道 data 里有什么
# 容易因为拼写错误导致运行时崩溃
if data.get('status') == 'active':
return data['items']

✅ 修改后 (Rust 风格的 Struct)

from pydantic import BaseModel, Field
from typing import List, Literal

# 定义数据形状 (Data Shape)
class Item(BaseModel):
name: str
price: float

class Payload(BaseModel):
# 使用 Literal 做枚举约束,类似 Rust 的 enum
status: Literal['active', 'inactive']
items: List[Item] = Field(default_factory=list)

# 函数签名即文档,IDE 甚至能补全 .status
def process_data(payload: Payload) -> List[Item]:
if payload.status == 'active':
return payload.items
return []

收益:你获得了类似静态语言的编译期(IDE 静态检查)安全感。你的函数签名不再撒谎。


层级 2:配置管理 (Rust Config 风格)

做后端和 AI 开发,经常需要读取 API Keys, DB Host, Model Names。许多人习惯用 os.getenv() 甚至硬编码,导致配置分散且不安全。

做法:使用 pydantic-settings

这非常像 Rust 的 Config crate,它将环境变量一次性加载为类型安全的对象。如果配置缺失或类型错误,程序启动即崩溃(Fail Fast),而不是在半夜运行到那行代码时才报错。

from pydantic_settings import BaseSettings

class AppConfig(BaseSettings):
openai_api_key: str
db_host: str = "localhost"
db_port: int = 5432
debug_mode: bool = False

class Config:
env_file = ".env" # 自动读取 .env 文件

# 初始化一次,全局使用单例
config = AppConfig()

# 使用时,完全类型安全
print(config.db_port + 1) # IDE 知道这是 int,完全放心的数学运算

收益:将配置加载从“运行时风险”变成了“启动时检查”。


层级 3:清洗即解析 (Parse, don't validate)

这是一个非常 Functional Programming (FP) 的理念:不要把验证看作是“检查”,而要看作是“数据变换”。

传统的验证是:给我数据 -> 我看看对不对 -> 抛出错误。 解析的思维是:给我数据 -> 我把它变成我要的格式 -> 给我干净的对象。

Pydantic 的 field_validator 允许你在数据实例化的瞬间清洗数据。

场景:AI 训练数据清洗

输入可能是 " 123 ", "123", 或 123,或者是逗号分隔的字符串,但你的领域对象需要统一的 List[str]

from pydantic import BaseModel, field_validator

class UserInput(BaseModel):
tag_list: list[str]

@field_validator('tag_list', mode='before')
@classmethod
def split_string_tags(cls, v):
# 脏活累活在这里处理
# 如果输入是 "a,b,c",自动切分为 ['a', 'b', 'c']
if isinstance(v, str):
return [x.strip() for x in v.split(',')]
return v

# 即使上游传了脏数据,这里也能自动清洗
data = UserInput(tag_list=" ai, python, rust ")
print(data.tag_list)
# Output: ['ai', 'python', 'rust']

收益:你的核心业务逻辑(Function)永远只处理干净、标准的数据。


层级 4:AI 结构化输出 (Structured Output)

这是目前 Pydantic 最前沿 的用法。作为 AI 开发者,你一定遇到过 LLM 输出 JSON 格式不稳定的问题(比如多了一个逗号,或者 key 拼错)。

做法:将 Pydantic 作为 Schema 协议,强制 LLM 输出符合你定义的数据结构。

这在 OpenAI 的 Function Calling 或 instructor 库中是核心模式。

import instructor
from openai import OpenAI
from pydantic import BaseModel

# 1. 定义你想要的结果结构
class UserInfo(BaseModel):
name: str
age: int
interests: list[str]

# 2. Patch 你的 OpenAI 客户端
client = instructor.from_openai(OpenAI())

# 3. 直接请求 Pydantic 对象,而不是文本
user_info = client.chat.completions.create(
model="gpt-4o",
response_model=UserInfo, # 关键:告诉 LLM 我只要这个结构
messages=[
{"role": "user", "content": "张三今年25岁,喜欢Rust和AI"}
],
)

# 4. 拿到的是真正的 Python 对象,不是 dict,也不是 json string
print(user_info.interests) # ['Rust', 'AI']

收益:将非结构化的自然语言(LLM Output)直接固化为代码可用的结构体。这是构建 AI Agent 的基石。


层级 5:作为数据库 Schema (SQLModel)

既然你用 FastAPI,你可能听说过 SQLModel。它是由 FastAPI 作者 Tiangolo 开发的,本质上就是 Pydantic + SQLAlchemy 的合体。

这意味着同一个 Class,既是:

  1. API 验证层 (Pydantic)
  2. 数据库表定义 (SQLAlchemy Table)
  3. 业务数据容器 (Data Class)
from sqlmodel import SQLModel, Field

# 一个类,打通前后端和数据库
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
age: int | None = None

收益:消除了 DTO (Data Transfer Object) 和 DAO (Data Access Object) 之间的重复定义。极其适合不喜欢写重复样板代码(Boilerplate)的开发者。


总结:你的“Pydantic 进化论”

从脚本小子到架构师,Pydantic 的使用深度折射了你的编程思维变化:

维度以前的做法 (Pythonic / Scripting)你的新做法 (Rust-like / Engineering)
核心思维只要代码能跑就行结构化思维,类型安全
函数传参def f(d: dict)def f(d: MyModel)
配置管理os.environ.get() 拼凑class Settings(BaseSettings)
错误处理运行时随缘报错Fail Fast (启动即检查)
数据清洗函数内手写 if/else@field_validator (Parse, don't validate)
AI 交互Prompt 工程 + 正则提取response_model=MyModel
整体架构逻辑与数据耦合Data Class (Pydantic) + Function (Logic)

一句话建议

在你的下一个 Python 项目中,尝试禁止在函数参数和返回值中使用 dict

强迫自己定义 Pydantic Model 来承载一切数据流动。你会发现,虽然写代码时的按键次数变多了(定义类),但调试代码的时间和运行时崩溃的概率会呈指数级下降。这就是结构化的力量。

鱼雪

在软件工程的演化史上,我们似乎经历了一个巨大的钟摆运动。

从 C 语言的自由狂放,到 Java/C++ 面向对象(OOP)的森严壁垒,再到如今 Rust 和 Go 的崛起。很多人认为这是一种“倒退”,因为它丢弃了继承,丢弃了复杂的类。

但实际上,这是一种螺旋上升的综合(Synthesis)

现代编程范式并非简单的“去 OOP 化”,而是站在 C 与 OOP 的中间,利用强大的类型系统作为粘合剂,达成了一种更高级的哲学境界:该聚合的聚合,该分离的分离。


第一阶段:C 语言的“完全分离” (The Thesis)

哲学:数据就是数据,代码就是代码。

在 C 语言的时代,世界是平铺直叙的。

  • 分离struct 定义内存布局,函数定义逻辑。两者在语法上没有必然联系。
  • 自由:你可以写一个函数去操作任何符合内存布局的数据。
  • 代价:这种分离过于彻底,导致了“内聚性”的缺失。数据缺乏保护,逻辑散落四处,全靠程序员的自律来维持秩序。

第二阶段:OOP 的“强行聚合” (The Antithesis)

哲学:世界是由对象组成的,数据必须和行为绑定。

为了解决 C 的混乱,OOP 走向了另一个极端。

  • 聚合:OOP 认为数据(属性)和行为(方法)必须死锁在一个 Class 容器里。
  • 约束:通过继承(Inheritance)来复用代码,通过 private 来保护数据。
  • 代价:这种聚合过于僵硬。为了复用一个简单的功能,你不得不继承庞大的父类(“香蕉与大猩猩问题”)。业务逻辑被层层叠叠的类结构锁死,修改变得极其困难。

第三阶段:现代范式 —— 中庸之道 (The Synthesis)

Rust、Go 等现代语言的出现,标志着我们终于在“分离”与“聚合”之间找到了平衡点。它们的设计哲学可以概括为:物理上分离,逻辑上聚合,类型上约束。

1. 该聚合的聚合:回归 Struct 与 模块化

我们不再需要庞大的 Class,但我们依然需要高内聚(High Cohesion)

  • 数据聚合:我们沿用了 C 语言的 struct,因为它在内存中是最紧凑、最高效的表达方式。
  • 封装聚合:我们没有退回到 C 的“裸奔”。通过现代语言的模块系统(Module System)(如 Rust 的 pub(crate),Go 的首字母大小写),我们将相关的数据和逻辑在模块/包的层面上聚合。
  • 以前是“类封装”,现在是“模块封装”。范围更宽,更灵活。

2. 该分离的分离:Trait 与 Interface

我们承认,**行为(Behavior)**往往是跨越数据类型的。

  • 行为分离:不管是 Duck 还是 Car,只要能 Run,就是可行的。现代语言将行为剥离为 Trait(特质)或 Interface(接口)。
  • 去耦合:逻辑不再硬编码在继承树里。行为变成了可以随意插拔的“插件”。

3. 关键变量:强大的类型系统 (The Type System)

这是区分“现代语言”与“C 语言”的核心区别。如果说 C 靠自律,OOP 靠类层级,那么现代语言靠的是编译器与类型系统

它在“分离”与“聚合”之间架起了一座安全的桥梁:

  • 代数数据类型 (ADT) / 枚举 (Enum): Rust 的 Enum 比 OOP 的多态更精准。它允许你定义“要么是 A,要么是 B”的数据结构(Sum Type),配合模式匹配(Pattern Matching),你可以在不使用继承的情况下,优雅地处理多种状态。
  • 泛型与约束 (Generics with Bounds)fn process<T: Speakable>(item: T) 这句话翻译过来就是:“我不关心 item 是什么数据结构(分离),但我要求它必须具备说话的能力(聚合约束)。”

案例对比:思维的跃迁

让我们看一个简单的例子:处理 HTTP 请求

  • C 语言思路:定义一个 struct Request,写一堆独立的函数 parse_request, handle_request。如果处理错了类型,运行时崩溃。
  • OOP 思路:定义 AbstractRequest 基类,派生 GetRequest, PostRequest。逻辑分散在继承树里,难以直观看到全貌。
  • 现代思路 (Rust)
  1. 聚合数据:用 struct 定义请求体。
  2. 类型约束:用 enum Method { GET, POST } 穷举所有可能性(编译器保证你不会漏掉任何一种情况)。
  3. 分离行为:定义 Handler trait。
  4. 组合:通过 impl Handler for Request 将它们联系起来。

结语

现代编程范式之所以迷人,是因为它不再执着于“万物皆对象”的教条,也不再容忍“指针满天飞”的混乱。

它汲取了 C 语言Struct的高效与直观,吸收了 OOP 封装的思想,最后引入了数学般严谨的类型系统

这就是**“该聚合的聚合,该分离的分离”**:

  • 让数据在内存中保持紧凑(聚合)。
  • 让行为在逻辑上保持解耦(分离)。
  • 让编译器在构建时保驾护航(类型系统)。

这也许才是软件工程成熟的标志。

鱼雪

Wasmi 1.0:轻量 WebAssembly 解释器终于稳定

鱼雪的AI博客-来自NotebookLM

0:00
0:00

原文链接:Wasmi 1.0 — WebAssembly Interpreter Stable At Last

自 2024 年 5 月发布上一版更新后,轻量级 WebAssembly 解释器 Wasmi 历经数月打磨,终于正式发布 1.0 稳定版。这个版本不仅清理了历史弃用 API,承诺未来接口稳定性,也标志着 Wasmi 在功能、性能、安全性上的成熟,可广泛应用于 IoT 设备、插件系统、云宿主、智能合约乃至游戏主机等场景。

1.0 的意义:API 与能力全面成熟

  • API 稳定承诺:1.0 起不再保留旧版弃用接口,开发者可放心升级。
  • 面向嵌入式场景:低内存占用、可预测执行、可与宿主良好集成,是嵌入式与资源受限环境的高性价比 Wasm Runtime。

支持的全新 Wasm 提案

自 2024 年 5 月以来,Wasmi 紧跟 Wasm 生态推进,完成多项提案实现:

  • multi-memory:单模块多线性内存,实现用途隔离。
  • memory64:支持 64 位寻址,突破 4GB 内存上限。
  • custom-page-sizes:最小 1 字节页尺寸,适配超小型设备。
  • simd / relaxed-simd:提供 128 位 SIMD 指令(可选启用),加速计算密集型任务。
  • wide-arithmetic:新增 128 位 add/sub/mul 运算,利于大整数应用。
  • tail-call、extended-const 等 Wasm 3.0 提案:提前支持未来标准。

凭借这些更新,Wasmi 已完整覆盖 Wasm 2.0,甚至领先实现多项 Wasm 3.0 能力。

引擎优化:性能与内存双提升

  • 内部重构:翻译引擎、字节码、执行循环经过大量清理与重写,执行效率和内存占用显著改善。
  • M2 Pro 基准测试:Coremark 等测试均显示较旧版本有稳定收益,尽管仍沿用 2024 年引入的解释器架构。

安全与可靠性提升

  1. 安全审计:Stellar Development Foundation 赞助 Runtime Verification 对 Wasmi 进行全面审计,发现的问题已全部修复。
  2. 模糊测试体系
    • translate:持续翻译随机 Wasm 数据,查找翻译器缺陷。
    • execute:以多输入执行 Wasm,挖掘执行引擎 Bug。
    • differential:与其它 Runtime(如 Wasmtime)对比执行,捕捉语义差异。
  3. OSS-Fuzz + Wast Testsuite:接入 Google OSS-Fuzz,并维护独立 Wast 测试集,覆盖 Wasmi 特有场景。
  4. 依赖收缩:外部依赖从 7 个降至 2 个(spinwasmparser),计划未来以自研 wasmi_parse 完全替换,进一步降低供应链风险。

新特性:从 C-API 到可续航 Fuel

  • C-API 绑定:通过 wasmi_c_api_impl,任何可调用 C 的语言都能直接集成 Wasmi。官方还计划借助 PyO3 提供 Python 绑定。
  • Refueled Resumable Calls:燃料耗尽时可暂停执行,稍后补充 Fuel 后继续,方便调度并发 Wasm 任务。
  • 易用性提升
    • Module::new / new_unchecked 在启用 wat feature 后可直接加载 WAT。
    • API 设计进一步贴近 Wasmtime,降低迁移成本。
    • 新增低层级 Instance::new、官方 Usage Guide、Hash/BTree 可选等功能。
    • 修复了在宿主函数内部编译新模块会死锁的问题。

展望:Wasmi 2.0 与 Wasm 3.0 全面兼容

  • 下一代解释器:2.0 将聚焦性能,提供多种指令派发模式(直接/间接 threaded dispatch、loop-match 等),并计划结合 Rust become 关键字、#[loop_match] 属性(待稳定)进一步提升效率。
  • Wasm 3.0 支持:目前尚缺 function-referencesexception-handlinggc 三项提案,待新引擎落地后将优先补齐。

如何体验 Wasmi 1.0

  • 作为库依赖:cargo add wasmi
  • CLI:cargo install wasmi_cli
  • C 语言接口:参考官方 C-API README
  • Wasmer Backend:开启 Wasmer 的 wasmi feature
  • 或直接使用已集成 Wasmi 的项目
鱼雪

大模型'手脚'延伸:深入解析AI工具调用与智能体进化之路

鱼雪的AI博客-来自NotebookLM

0:00
0:00

目录

  1. 引言
  2. 大模型工具调用基础原理
  3. 工具调用流程详解
  4. Rust 实现案例分析
  5. 代码架构设计
  6. 最佳实践与优化
  7. 总结

引言

随着大语言模型(LLM)技术的快速发展,单纯的文本生成已经无法满足复杂应用场景的需求。**工具调用(Tool Calling)**作为一种重要的扩展机制,让大模型能够与外部系统交互,获取实时数据,执行特定任务,从而构建更加智能和实用的 AI 应用。

本文将深入探讨大模型工具调用的核心原理,并通过一个完整的 Rust 项目案例,详细讲解如何实现一个支持天气查询和网络搜索的智能助手,使用原生的HTTP调用方式,不使用任何第三方LLM服务的客户端,比如Python的openai库,这样便于对原理有更清晰的理解,也便于根据任何编程语言开发自己工具,而不局限于特定库。

大模型工具调用算是LLM中一种动态获取,或说实时获取与用户提问相关数据的一种方式,为用户的问题提供可靠的上下文。另外一种方式基于RAG的方式,这种是基于知识库,更像是一种静态知识。之后会用别的文章再来讨论,敬请期待。

关键词

  • 大语言模型 (Large Language Model, LLM)
  • 工具调用 (Tool Calling)
  • Function Calling
  • AI Agent
  • Rust 异步编程
  • DeepSeek API

大模型工具调用基础原理

什么是工具调用?

LLM 工具调用是指大语言模型在生成回答时,能够识别用户需求并主动调用预定义的外部函数或 API 来获取信息或执行操作的能力。这种机制让 LLM 从纯文本生成器转变为能够与现实世界交互的智能代理。

核心优势

  1. 实时性:获取最新的数据和信息
  2. 准确性:避免模型幻觉,提供可靠的事实信息
  3. 扩展性:通过工具扩展模型能力边界
  4. 可控性:明确的函数调用过程,便于调试和监控

工具调用流程详解

完整流程图

---
id: f662e516-56f0-46a1-a622-1438ab60949c
---
sequenceDiagram
participant User as 用户
participant App as 应用程序
participant LLM as 大语言模型
participant Tool as 外部工具/API

User->>App: 发送查询请求
App->>LLM: 用户请求 + 工具函数描述(JSON对象)
LLM->>App: 选择的工具函数 + 工具调用参数
App->>Tool: 匹配实际工具函数填充参数并调用
Tool->>App: 获取实际工具函数执行结果
App->>LLM: 用户请求 + 工具函数执行结果
LLM->>App: 生成最终回答
App->>User: 返回完整回答

大模型工具调用基础原理

详细步骤分析

1. 初始请求阶段

用户向系统发送查询请求,应用程序将请求连同预定义的函数工具描述一起发送给大模型。

{
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "你是一个专业的助手,可以提供天气信息和搜索功能"
},
{
"role": "user",
"content": "今天上海的天气怎么样?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "获取指定城市的天气预报信息",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "城市名称"
}
},
"required": ["location"]
}
}
}
],
"tool_choice": "auto"
}

2. 模型决策阶段

大模型分析用户请求,识别(根据工具的描述文档,然后自行决定调用需要调用哪些工具)出需要调用天气查询工具,并返回结构化的工具调用指令(函数名 + 与之匹配的参数):

{
"choices": [
{
"message": {
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_123",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"上海\"}"
}
}
]
}
}
]
}

3. 工具执行阶段

应用程序解析工具调用指令,执行实际的 API 调用:

// 解析工具调用参数
let args: Value = serde_json::from_str(
call["function"]["arguments"].as_str().unwrap()
)?;
let location = args["location"].as_str().unwrap();

// 调用实际的天气 API
let weather_info = amap::get_weather(location, &amap_key).await?;

4. 结果整合阶段

将工具执行结果反馈给大模型,生成最终的用户友好回答:

{
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "你是一个专业的天气顾问,请根据天气数据给出详细建议"
},
{
"role": "assistant",
"content": null,
"tool_calls": [...]
},
{
"role": "tool",
"content": "天气数据JSON",
"tool_call_id": "call_123"
}
]
}

Rust 实现案例分析

项目架构概览

我们的项目采用模块化设计,主要包含以下组件:

src/
├── main.rs # 主程序入口和聊天逻辑
├── tools/ # 工具模块
│ ├── mod.rs # 工具注册和统一接口
│ ├── amap.rs # 高德天气 API 工具
│ └── serper.rs # Google 搜索 API 工具

核心代码实现

1. 主聊天逻辑

async fn chat(user_query: &str) -> Result<(), Box<dyn std::error::Error>> {
let api_key = env::var("DEEPSEEK_API_KEY").unwrap();
let endpoint = env::var("DEEPSEEK_API_URL").unwrap();

// 构建包含工具定义的请求
let body = json!({
"model": env::var("MODEL_NAME").unwrap(),
"messages": [
{
"role": "system",
"content": "你是一个专业的助手,可以提供天气信息和搜索功能"
},
{"role": "user", "content": user_query}
],
"tools": get_tools_definition(),
"tool_choice": "auto", // 使用`auto`让大模型自行决定使用哪些工具
});

// 发送初始请求
let response = reqwest::Client::new()
.post(&endpoint)
.header("Authorization", format!("Bearer {}", api_key))
.json(&body)
.send()
.await?
.json::<Value>()
.await?;

// 处理工具调用
if let Some(tool_calls) = response["choices"][0]["message"]["tool_calls"].as_array() {
for call in tool_calls {
let tool_result = handle_tool_call(call).await?;

// 将结果反馈给模型生成最终回答
let final_response = generate_final_response(&api_key, &endpoint, call, &tool_result).await?;

info!("最终回答: {}", final_response["choices"][0]["message"]["content"]);
}
}

Ok(())
}

2. 工具注册系统

为了实现高内聚、低耦合的设计,我们创建了一个统一的工具注册系统:

// 工具处理结果结构
pub struct ToolResult {
pub content: String,
pub system_prompt: String,
}

// 工具处理函数类型
type ToolHandler = Box<dyn Fn(&Value) -> Pin<Box<dyn Future<Output = Result<ToolResult, Box<dyn std::error::Error>>> + Send + Sync>> + Send + Sync>;

// 工具注册表
struct ToolRegistry {
tools: HashMap<String, ToolHandler>,
}

impl ToolRegistry {
fn new() -> Self {
let mut tools = HashMap::new();

// 注册天气查询工具
let weather_handler: ToolHandler = Box::new(|call: &Value| {
let call = call.clone();
Box::pin(async move {
let args: Value = serde_json::from_str(call["function"]["arguments"].as_str().unwrap())?;
let location = args["location"].as_str().unwrap();

let amap_key = env::var("AMAP_API_KEY").unwrap();
let weather_info = amap::get_weather(location, &amap_key).await?;

Ok(ToolResult {
content: serde_json::to_string(&weather_info)?,
system_prompt: "你是一个专业的天气顾问,请根据获取到的天气数据给出详细的穿衣建议".to_string(),
})
})
});
tools.insert("get_weather".to_string(), weather_handler);

// 注册搜索工具
let search_handler: ToolHandler = Box::new(|call: &Value| {
let call = call.clone();
Box::pin(async move {
let args: Value = serde_json::from_str(call["function"]["arguments"].as_str().unwrap())?;
let query = args["query"].as_str().unwrap();

let search_results = serper::search(query).await?;
let formatted_results = serper::format_results(&search_results, 3);

Ok(ToolResult {
content: formatted_results,
system_prompt: "你是一个专业的信息分析师,请根据搜索结果给出准确、简洁的回答".to_string(),
})
})
});
tools.insert("search".to_string(), search_handler);

Self { tools }
}
}

// 统一的工具调用处理函数
pub async fn handle_tool_call(call: &Value) -> Result<ToolResult, Box<dyn std::error::Error>> {
let tool_name = call["function"]["name"].as_str().ok_or("无效的工具名称")?;
let registry = get_registry();

match registry.get_handler(tool_name) {
Some(handler) => handler(call).await,
None => Err(format!("未知的工具调用: {}", tool_name).into()),
}
}

3. 天气查询工具实现

pub async fn get_weather(location: &str, api_key: &str) -> Result<WeatherResponse, reqwest::Error> {
// 第一步:获取城市行政编码
let district_url = format!(
"https://restapi.amap.com/v3/config/district?key={}&keywords={}&subdistrict=0&extensions=all",
api_key, location
);

let district_resp: DistrictResponse = reqwest::get(&district_url).await?.json().await?;

if district_resp.status != "1" || district_resp.districts.is_empty() {
return Err(reqwest::get("http://error").await.unwrap_err());
}

let adcode = &district_resp.districts[0].adcode;

// 第二步:获取天气数据
let weather_url = format!(
"https://restapi.amap.com/v3/weather/weatherInfo?key={}&city={}&extensions=all&output=json",
api_key, adcode
);

let weather_resp: WeatherResponse = reqwest::get(&weather_url).await?.json().await?;

if weather_resp.status != "1" {
return Err(reqwest::get("http://error").await.unwrap_err());
}

Ok(weather_resp)
}

4. 搜索工具实现

pub async fn search(query: &str) -> Result<Vec<SearchResult>, reqwest::Error> {
let api_key = env::var("SERPER_API_KEY").expect("SERPER_API_KEY must be set");
let client = reqwest::Client::new();

let response = client
.post("https://google.serper.dev/search")
.header("X-API-KEY", api_key)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"q": query,
"type": "search"
}))
.send()
.await?
.json::<SearchResponse>()
.await?;

let results = response
.organic
.into_iter()
.map(|result| SearchResult {
title: result.title,
link: result.link,
snippet: result.snippet,
})
.collect();

Ok(results)
}

pub fn format_results(results: &[SearchResult], max_results: usize) -> String {
let results = results
.iter()
.take(max_results)
.enumerate()
.map(|(i, result)| {
format!(
"{}. {}\n 链接: {}\n 摘要: {}\n",
i + 1,
result.title,
result.link,
result.snippet
)
})
.collect::<Vec<_>>()
.join("\n");

if results.is_empty() {
"没有找到相关结果。".to_string()
} else {
results
}
}

代码架构设计

设计原则

我们的实现遵循以下软件设计原则:

  1. 高内聚:相关功能集中在同一模块中
  2. 低耦合:模块间依赖关系最小化
  3. 模块化:清晰的模块边界和职责分离
  4. 可扩展性:易于添加新的工具和功能

模块职责划分

  • main.rs:负责主程序逻辑和 LLM 交互
  • tools/mod.rs:工具注册表和统一接口
  • tools/amap.rs:天气查询功能实现
  • tools/serper.rs:搜索功能实现

异步编程模式

项目大量使用 Rust 的异步编程特性:

// 使用 tokio 运行时
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 异步函数调用
chat(user_query).await?;
Ok(())
}

// 异步工具处理函数
type ToolHandler = Box<dyn Fn(&Value) -> Pin<Box<dyn Future<Output = Result<ToolResult, Box<dyn std::error::Error>>> + Send + Sync>> + Send + Sync>;

最佳实践与优化

1. 错误处理

使用 Rust 的 Result 类型进行优雅的错误处理:

pub async fn handle_tool_call(call: &Value) -> Result<ToolResult, Box<dyn std::error::Error>> {
let tool_name = call["function"]["name"].as_str().ok_or("无效的工具名称")?;
// ... 其他逻辑
}

2. 环境变量管理

使用 dotenv 管理敏感配置:

use dotenv::dotenv;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenv().ok();
let api_key = env::var("DEEPSEEK_API_KEY").unwrap();
// ...
}

3. 日志记录

使用 tracing 进行结构化日志记录:

use tracing::{Level, info};

info!("用户查询: {}", user_query);
info!("工具调用参数: {}", serde_json::to_string_pretty(&args).unwrap());

4. 性能优化

  • 使用连接池复用 HTTP 连接
  • 实现请求缓存机制
  • 并发处理多个工具调用

5. 安全考虑

  • API 密钥安全存储
  • 输入参数验证
  • 请求频率限制

扩展功能建议

1. 添加更多工具

// 数据库查询工具
let db_handler: ToolHandler = Box::new(|call: &Value| {
// 数据库查询逻辑
});

// 文件操作工具
let file_handler: ToolHandler = Box::new(|call: &Value| {
// 文件操作逻辑
});

2. 实现工具链

支持多个工具的串联调用,实现复杂的业务流程。

3. 添加缓存机制

use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::Mutex;

struct CacheManager {
cache: Arc<Mutex<HashMap<String, String>>>,
}

4. 监控和指标

添加工具调用的性能监控和成功率统计。

总结

大模型工具调用是构建智能 AI 应用的关键技术。通过本文的详细分析,我们了解了:

  1. 工具调用的基本原理:从用户请求到最终回答的完整流程
  2. 实现架构设计:模块化、可扩展的代码组织方式
  3. 具体代码实现:使用 Rust 构建高性能的工具调用系统
  4. 最佳实践:错误处理、日志记录、性能优化等方面的建议

这种架构不仅提供了良好的代码组织结构,还为后续功能扩展奠定了坚实基础。随着大模型技术的不断发展,工具调用将成为构建复杂 AI 应用的标准模式。

相关资源


本文展示了如何使用 Rust 构建支持工具调用的大模型应用。如果您对实现细节有疑问,欢迎查看完整的项目源码或提出 Issue 讨论。

鱼雪

发布日期:2025 年 2 月 20 日
作者:Rust 发布团队

Rust 团队欣喜地宣布推出 Rust 编程语言的新版本 Rust 1.85.0,并正式稳定 Rust 2024 版本。 Rust 是一款赋予开发者构建可靠、高效软件能力的编程语言。 本文将详细介绍如何升级至 Rust 1.85.0、Rust 2024 的主要更新内容以及迁移指南。


如何升级到 Rust 1.85.0

如果您已通过 rustup 安装了 Rust,只需运行以下命令即可升级到 1.85.0:

rustup update stable

如果您尚未安装 rustup,请访问 Rust 官方网站的相关页面获取安装程序, 并查看 Rust 1.85.0 详细发布说明

想参与未来版本的测试?您可以本地切换到 betanightly 通道:

  • Beta 通道:rustup default beta
  • Nightly 通道:rustup default nightly

欢迎报告您发现的任何问题!


Rust 1.85.0 稳定版亮点:Rust 2024 正式发布

Rust 2024 版本概览

我们激动地宣布 Rust 2024 版本 现已稳定!Rust 的“版本”(Edition)机制允许开发者选择性采用可能影响向后兼容性的更改。 详情请参阅版本指南,了解实现方式及迁移步骤。

Rust 2024 是迄今为止我们发布的最大版本。 以下是主要更新总结,具体细节可参考版本指南

语言特性更新

  1. RPIT 生命周期捕获规则

    • 当未使用 use<..> 时,impl Trait 类型的默认参数捕获规则发生变化。
  2. if let 临时变量作用域

    • if let 表达式的临时变量作用域调整。
  3. 尾表达式临时变量作用域

    • 块中尾部表达式的临时变量作用域调整。
  4. Match 人体工程学预留

    • 禁止某些模式组合,以避免混淆并为未来改进预留空间。
  5. 不安全的外部块

    • extern 块现需使用 unsafe 关键字。
  6. 不安全属性

    • export_namelink_sectionno_mangle 属性需标记为 unsafe
  7. unsafe_op_in_unsafe_fn 警告

    • 该 lint 现默认警告,需在不安全函数中使用显式的 unsafe {} 块。
  8. 禁止引用 static mut

    • static mut 项的引用现默认生成错误。
  9. Never 类型回退调整

    • ! 类型强制转换规则调整,never_type_fallback_flowing_into_unsafe lint 级别改为“拒绝”。
  10. 宏片段说明符

    • macro_rules! 宏中的 expr 片段说明符现也匹配 const_ 表达式。
  11. 缺少宏片段说明符

    • missing_fragment_specifier lint 现为硬性错误,拒绝未指定片段类型的宏元变量。
  12. gen 关键字预留

    • 为未来添加生成器块预留 gen 关键字。
    • 预留语法
      为未来解析受保护字符串字面量预留 #\"foo\"# 样式字符串和 ## 标记。

标准库更新

  1. 预导入模块调整

    • FutureIntoFuture 添加至预导入模块。
  2. Box<[T]> 实现 IntoIterator

    • 调整 boxed 切片与迭代器的交互方式。
  3. 新增不安全函数

    • std::env::set_varstd::env::remove_varstd::os::unix::process::CommandExt::before_exec 现为不安全函数。

Cargo 更新

  1. Rust 版本感知解析器

    • 默认依赖解析器行为调整,考虑 rust-version 字段。
  2. 表和键名一致性

    • 移除部分过时的 Cargo.toml 键。
  3. 拒绝未使用的继承默认特性

    • 调整 default-features = false 与继承工作区依赖的交互。

Rustdoc 更新

  1. 合并测试

    • 文档测试现合并为单个可执行文件,显著提升性能。
  2. 嵌套 include! 调整

    • 嵌套 include! 文件的相对路径行为变更。

Rustfmt 更新

  1. 样式版本

    • 引入“样式版本”概念,允许独立控制格式化版本与 Rust 版本。
  2. 格式化修复

    • 大量修复针对不同情况的格式化问题。
  3. 原始标识符排序

    • 调整 r#foo 标识符的排序方式。
  4. 版本排序

    • 调整包含整数的标识符排序方式。

如何迁移至 Rust 2024

版本指南 提供了所有新功能的迁移说明及现有项目升级至新版本的通用步骤。 许多情况下,cargo fix 可自动完成必要更改,甚至您的代码可能无需任何变动即可适配 Rust 2024!

需要注意的是,cargo fix 的自动修复非常保守,避免更改代码语义。 您可能希望保留原有代码并使用 Rust 2024 的新语义,例如继续使用 expr 宏匹配器, 或忽略条件语句的转换以采用新的 2024 丢弃顺序语义。cargo fix 的结果仅为保守转换,不代表推荐做法。

感谢众多贡献者共同打造这一版本!


异步闭包(Async Closures)

Rust 现支持异步闭包,例如 async || {},调用时返回 future。 这类似于 async fn,可以捕获局部环境变量,类似于普通闭包与函数的区别。 标准库预导入中新增三个相关 traitAsyncFnAsyncFnMutAsyncFnOnce

过去,您可能通过普通闭包和异步块(如 || async {})实现类似效果, 但内部块返回的 future 无法借用闭包捕获的值。 而异步闭包解决了这一问题:

let mut vec: Vec<String> = vec![];

let closure = async || {
vec.push(ready(String::from("")).await);
};

此外,异步闭包还支持使用 AsyncFn trait 表达高阶函数签名,返回 Future

use core::future::Future;

async fn f<Fut>(_: impl for<'a> Fn(&'a u8) -> Fut)
where
Fut: Future<Output = ()>,
{ todo!() }

async fn f2(_: impl for<'a> AsyncFn(&'a u8))
{ todo!() }

async fn main() {
async fn g(_: &u8) { todo!() }
f(g).await; // 类型不匹配错误
f2(g).await; // 正常运行!
}

异步闭包为这些问题提供了优雅的解决方案! 详情请参阅 RFC 3668稳定报告


从诊断中隐藏 trait 实现

新增 #[diagnostic::do_not_recommend] 属性,提示编译器不在诊断信息中显示指定的 trait 实现。 这对库作者很有用,可避免编译器提出无用或误导性的建议。 例如:

pub trait Foo {}
pub trait Bar {}

#[diagnostic::do_not_recommend]
impl<T: Foo> Bar for T {}

struct MyType;

fn main() {
let _object: &dyn Bar = &MyType;
}

添加此属性后,错误信息将更清晰,不再提及无关的 Foo 建议。 详情请参阅 RFC 2397


元组的 FromIteratorExtend 支持

Rust 1.85.0 扩展了对元组的支持,新增从单一 (T,) 到 12 项 (T1, T2, ..., T12)FromIteratorExtend 实现。

例如:

use std::collections::{LinkedList, VecDeque};

fn main() {
let (squares, cubes, tesseracts): (Vec<_>, VecDeque<_>, LinkedList<_>) =
(0i32..10).map(|i| (i * i, i.pow(3), i.pow(4))).collect();
println!("{squares:?}");
println!("{cubes:?}");
println!("{tesseracts:?}");
}

输出:

[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
[0, 1, 8, 27, 64, 125, 216, 343, 512, 729]
[0, 1, 16, 81, 256, 625, 1296, 2401, 4096, 6561]

std::env::home_dir() 更新

std::env::home_dir() 已弃用多年,因其在某些 Windows 配置下可能返回意外结果。 现调整其行为作为 bug 修复,后续版本将移除弃用警告。


稳定化的 API

以下 API 已稳定并支持 const 上下文:

  • BuildHasherDefault::new
  • ptr::fn_addr_eq
  • io::ErrorKind::QuotaExceeded
  • io::ErrorKind::CrossesDevices
  • {float}::midpoint
  • Unsigned {integer}::midpoint
  • NonZeroU*::midpoint
  • 元组(1 到 12 元)的 std::iter::ExtendFromIterator<(A, ...)>
  • std::task::Waker::noop
  • mem::size_of_val
  • mem::align_of_val
  • Layout::for_value

其他更新

查看 RustCargoClippy 的完整变更日志。

鱼雪

深入 Dioxus 信号系统

1. 创建信号

在 Dioxus 中,可以使用 use_state 钩子创建状态:

let count = use_state(&cx, || 0);
  • 解释use_state 接受一个上下文引用 &cx 和一个初始化函数,返回一个状态和一个用于更新状态的函数。

2. 读取和更新信号

读取和更新状态的方式如下:

let current_value = *count; // 读取当前值
count.set(new_value); // 设置新值
  • 解释
    • *count:解引用以获取当前状态的值。
    • count.set(new_value):更新状态为 new_value,并触发组件重新渲染。

3. 状态更新的核心原理:基于 PartialEq 的变化检测

Dioxus 的信号系统通过底层实现的 PartialEq trait 来判断状态值的变化,从而决定是否触发组件的重新渲染。

原理讲解

  1. 信号变化检测

    • 每次调用 set 方法更新信号时,Dioxus 会通过调用值的 PartialEq 方法比较新值和旧值。
    • 如果 PartialEq::eq 返回 true(即值相等),Dioxus 会跳过重新渲染。
    • 如果 PartialEq::eq 返回 false(即值不相等),Dioxus 会将信号标记为“已变化”,并通知依赖此信号的组件重新渲染。
  2. 示例代码

    fn Counter(cx: Scope) -> Element {
    let count = use_state(&cx, || 0);

    cx.render(rsx! {
    button {
    onclick: move |_| count.set(*count + 1),
    "Count: {count}"
    }
    })
    }
    • count.set(*count + 1) 被调用时:
      1. count 的新值(*count + 1)与旧值(*count)通过 PartialEq 比较。
      2. 如果值不同,则触发组件重新渲染。
  3. 为什么使用 PartialEq

    • 性能优化:通过 PartialEq 判断是否需要重新渲染,避免不必要的性能开销。
    • 灵活性:支持自定义比较逻辑。开发者可以为自定义类型实现 PartialEq,仅比较关键字段或按需定义相等性规则。
  4. 自定义类型的 PartialEq 实现: 如果你有一个自定义数据类型,可以通过实现 PartialEq 控制变化检测逻辑。例如:

    #[derive(Clone)]
    struct CustomData {
    field1: i32,
    field2: String,
    }

    // 实现 PartialEq
    impl PartialEq for CustomData {
    fn eq(&self, other: &Self) -> bool {
    self.field1 == other.field1 // 仅比较 field1
    }
    }

原理优势

  • 避免冗余渲染:与 React 的 Virtual DOM diff 不同,Dioxus 在状态变化时直接通过 PartialEq 比较值,大幅减少无效计算。
  • 精细化控制:开发者可以通过自定义 PartialEq 提供更高的灵活性。
  • 内置优化:对基本类型如 i32StringPartialEq 比较成本极低。

4. 小结

Dioxus 的信号系统基于 PartialEq 实现细粒度的变化检测和状态更新机制。这种机制为开发者提供了:

  1. 高效的状态管理:减少不必要的重新渲染,提高性能。
  2. 灵活的比较逻辑:通过自定义 PartialEq 实现更复杂的状态变化判断。
  3. 简单的 API:通过自动追踪和更新依赖,简化组件开发。

这种自动化和精细化的状态管理方案,使得 Dioxus 在响应式编程中具有独特优势,为开发者带来更佳的开发体验。

鱼雪

引言

在现代前端开发中,状态管理是一个核心话题。React 通过 useStateuseReducer 等 Hooks 实现状态管理, 而 Dioxus 则采用了基于信号(Signal)的响应式方案。

本文将深入对比这两种方案的异同,帮助你更好地理解和使用 Dioxus 的状态管理系统。

基础概念对比

React 的状态管理

在 React 中,我们通常这样管理状态:

function Counter() {
const [count, setCount] = useState(0);

return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}

Dioxus 的信号系统

而在 Dioxus 中,使用信号来管理状态:

fn Counter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);

cx.render(rsx! {
button {
onclick: move |_| count.set(*count + 1),
"Count: {count}"
}
})
}

核心区别

  1. 状态更新机制

    • React: 通过 setState 触发重渲染。
    • Dioxus: 通过信号的变化自动触发组件更新, 本质上通过 PartialEq 实现自动更新
  2. 响应式特性

    • React: 需要手动处理依赖关系(如在 useEffect 中指定依赖)。
    • Dioxus: 自动追踪依赖,实现细粒度更新。

深入 Dioxus 信号系统

1. 创建信号

在 Dioxus 中,可以使用 use_state 钩子创建状态:

let count = use_state(&cx, || 0);
  • 解释use_state 接受一个上下文引用 &cx 和一个初始化函数,返回一个状态和一个用于更新状态的函数。

2. 读取和更新信号

读取和更新状态的方式如下:

let current_value = *count; // 读取当前值
count.set(new_value); // 设置新值
  • 解释
    • *count:解引用以获取当前状态的值。
    • count.set(new_value):更新状态为 new_value,并触发组件重新渲染。

3. 派生状态

在 React 中:

function DoubleCounter() {
const [count, setCount] = useState(0);
const doubleCount = useMemo(() => count * 2, [count]);

return <div>Double: {doubleCount}</div>;
}

在 Dioxus 中,可以使用 use_memo 创建派生状态:

fn DoubleCounter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
let double_count = use_memo(&cx, move || *count * 2, (*count,));

cx.render(rsx! {
div { "Double: {double_count}" }
})
}
  • 解释
    • use_memo 接受上下文引用、计算函数和依赖元组,返回计算结果。当依赖发生变化时,重新计算。

4. 异步状态处理

Dioxus 提供了 use_future 用于处理异步状态:

fn AsyncCounter(cx: Scope) -> Element {
let count = use_state(&cx, || 0);

let async_value = use_future(&cx, (*count,), |&count| async move {
// 异步操作
gloo_timers::future::TimeoutFuture::new(100).await;
count * 2
});

cx.render(rsx! {
div { "Async value: {async_value.value().unwrap_or(&0)}" }
})
}
  • 解释
    • use_future 接受上下文引用、依赖元组和异步函数,返回一个包含异步结果的状态。当依赖变化时,重新执行异步函数。

状态共享方案对比

1. Props 传递

React:

function Parent() {
const [count, setCount] = useState(0);
return <Child count={count} setCount={setCount} />;
}

Dioxus:

fn Parent(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
cx.render(rsx! {
Child { count: *count, set_count: count.setter() }
})
}
  • 解释:在 Dioxus 中,count.setter() 返回一个用于更新状态的函数,可以作为属性传递给子组件。

2. Context 使用

React:

const CountContext = createContext();

function Provider({ children }) {
const [count, setCount] = useState(0);
return (
<CountContext.Provider value={{ count, setCount }}>
{children}
</CountContext.Provider>
);
}

Dioxus:

#[derive(Clone)]
struct CountState {
count: UseState<i32>,
}

fn Provider(cx: Scope) -> Element {
let count = use_state(&cx, || 0);
cx.provide_context(CountState { count });

cx.render(rsx! {
// 子组件
})
}
  • 解释provide_context 用于在组件树中提供上下文,子组件可以使用 use_context 获取共享的状态。

3. 全局状态

Dioxus 支持全局状态管理:

static COUNT: AtomRef<i32> = |_| 0;

fn GlobalCounter(cx: Scope) -> Element {
let count = use_atom_ref(&cx, COUNT);
cx.render(rsx! {
button {
onclick: move |_| *count.write() += 1,
"Global count: {count.read()}"
}
})
}
  • 解释
    • AtomRef 定义了一个全局状态。
    • use_atom_ref 用于在组件中访问全局状态的引用,readwrite 方法用于读取和修改全局状态的值。

最佳实践建议

  1. 选择合适的状态范围

    • 局部状态:使用 use_state
    • 共享状态:使用 Context
    • 全局状态:使用 AtomRef
  2. 性能优化

    • 合理使用 use_memo 缓存派生状态。
    • 避免状态嵌套过深。
    • 尽量将状态分解为粒度更细的信号。
  3. 组件设计

    • 使用明确的属性接口。
    • 将复杂状态逻辑抽象为自定义 Hook。
    • 对于全局状态,尽量使用模块化设计避免耦合。

总结

Dioxus 的信号系统提供了一种更加自动化和细粒度的状态管理方案。相比 React 的手动依赖追踪,它能够:

  1. 自动追踪和更新依赖。
  2. 提供更简洁的 API。
  3. 实现更高效的细粒度更新。
  4. 更自然地处理异步状态。

虽然学习曲线可能稍陡,但掌握后能够带来更好的开发体验和运行时性能。

参考资料


本文首发于 https://yuxuetr.com,转载请注明出处。

鱼雪