13.6 Rhai 脚本引擎

`rust-agent-rhai` 将 Rhai 嵌入式脚本语言集成到 RAF 框架中,提供三种使用模式:`RhaiRuntime`(独立运行时)、`RhaiExecutor`(工作流节点执行器)和 `RhaiTool`(Agent 工具)。

架构概览

graph TB
    subgraph "Rhai 组件"
        RT[RhaiRuntime]
        RE[RhaiExecutor]
        RTOOL[RhaiTool]
    end

    subgraph "工作流集成"
        IEX[IExecutor trait]
        WFN[Workflow Node]
    end

    subgraph "Agent 集成"
        ITT[ITool trait]
        TR[ToolRegistry]
    end

    RT --> RE
    RT --> RTOOL
    RE -->|implements| IEX
    IEX --> WFN
    RTOOL -->|implements| ITT
    ITT --> TR

RhaiRuntime — 独立运行时

`RhaiRuntime` 是 Rhai 脚本引擎的核心封装,提供沙箱化执行环境:

use rust_agent_rhai::RhaiRuntime;
use rhai::Dynamic;

let mut runtime = RhaiRuntime::new();

// 注入变量
runtime.with_variable("threshold", Dynamic::from(42_i64));
runtime.with_json_variable("config", serde_json::json!({
    "mode": "production",
    "max_retries": 3
}));

// 设置并编译脚本
runtime.with_script(r#"
    let count = 0;
    let results = [];
    for item in config.items {
        if item.score > threshold {
            count += 1;
            results.push(item.name);
        }
    }
    #{count: count, high_scorers: results}
"#);

// 执行脚本
let result = runtime.run()?;
println!("结果: {:?}", result);

核心功能

| 功能 | 方法 | 说明 | |------|------|------| | 编译脚本 | `with_script(src)` | 设置并编译 Rhai 源代码 | | 注入变量 | `with_variable(name, value)` | 注入 Dynamic 类型变量 | | JSON 变量 | `with_json_variable(name, value)` | 注入 serde_json::Value(自动转换) | | 注册模块 | `with_module(name, module)` | 注册自定义 Rhai 模块 | | 注册类型 | `register_type::<T>()` | 注册自定义类型 | | 执行脚本 | `run()` | 运行预编译脚本,返回 JSON | | 一步执行 | `eval(script)` | 编译 + 执行一步完成 | | 表达式求值 | `eval_expression(expr)` | 求值单个表达式,返回 Dynamic | | 获取变量 | `get_variable(name)` | 从作用域中读取变量 |

沙箱安全

`RhaiRuntime` 使用 `Engine::new_raw()` 创建沙箱化引擎,默认禁用危险的 `eval` 函数:

let mut runtime = RhaiRuntime::new();

// 以下脚本会执行失败(eval 被禁用)
runtime.with_script("eval(\"42\")");
let result = runtime.run();
assert!(result.is_err()); // 沙箱阻止了 eval 调用

操作限制默认设为 100,000 次,可通过 `max_operations()` 调整:

runtime.max_operations(1_000_000); // 提高限制

RhaiExecutor — 工作流节点执行器

`RhaiExecutor` 将 Rhai 脚本适配为工作流的 `IExecutor`,可以直接作为工作流图的节点使用:

use rust_agent_rhai::RhaiExecutor;
use rust_agent_workflow::WorkflowBuilder;

let executor = RhaiExecutor::new(
    "data_transformer",     // 执行器 ID
    r#"
        // 输入变量: input (绑定为上游消息)
        // 上下文变量: node_id, context, _meta
        // 回调函数: emit_text(), emit_custom(), set_output()

        let transformed = #{
            original: input.data,
            processed: input.data.to_upper(),
            timestamp: _meta.node_id,
        };

        emit_text("数据转换完成");
        set_output("last_result", transformed.processed);

        transformed
    "#,
    "input",               // 输入变量绑定名称
);

// 用于工作流图
let mut builder = WorkflowBuilder::new("data_pipeline");
builder.add_node("transform", executor, "数据转换");

脚本内置变量

| 变量 | 类型 | 说明 | |------|------|------| | `input` (或自定义名) | Dynamic | 上游节点的输出消息 | | `node_id` | String | 当前执行节点的 ID | | `context` | Map | 当前工作流上下文状态快照 | | `_meta` | Map | 执行元信息(node_id 等) |

脚本回调函数

| 函数 | 说明 | |------|------| | `emit_text(msg)` | 发送流式文本进度事件 | | `emit_custom(key, value)` | 发送自定义进度事件 | | `set_output(key, value)` | 回写状态到工作流上下文 |

执行流程

#[async_trait]
impl IExecutor for RhaiExecutor {
    async fn handle(
        &self,
        message: Arc<dyn Any + Send + Sync>,
        ctx: &dyn IWorkflowContext,
        progress: UnboundedSender<NodeProgress>,
    ) -> Result<HandlerResult> {
        // 1. 提取输入数据
        let input_value = extract_input(message);

        // 2. 加载上下文快照
        let context_snapshot = load_context_snapshot(ctx).await?;

        // 3. 注入变量和回调,执行脚本
        let mut runtime = self.runtime.lock();
        runtime.with_json_variable(&self.input_var, input_value);
        runtime.scope_mut().push("node_id", node_id.clone());
        runtime.with_json_variable("context", context_snapshot);
        runtime.with_json_variable("_meta", serde_json::json!({
            "node_id": node_id,
        }));

        // 注册进度回调
        runtime.engine_mut().register_fn("emit_text", move |msg: &str| {
            let _ = progress.send(NodeProgress::TextDelta(msg.to_string()));
        });
        runtime.engine_mut().register_fn("set_output", move |key: &str, value: &str| {
            let val = serde_json::from_str::<Value>(value).unwrap_or(Value::String(value.into()));
            output_writes.lock().push((key.to_string(), val));
        });

        let result = runtime.run()?;

        // 4. 回写 output_writes 到上下文
        // 5. 返回 HandlerResult
        Ok(HandlerResult::Messages(vec![Arc::new(result)]))
    }
}

RhaiTool — Agent 工具

`RhaiTool` 将 Rhai 脚本封装为 `ITool`,使 Agent 可以通过 function calling 调用:

use rust_agent_rhai::RhaiTool;
use rust_agent_core::ITool;

let tool = RhaiTool::new(
    "calculate_discount",       // 工具名称
    "计算订单折扣金额",           // 工具描述
    serde_json::json!({         // JSON Schema
        "type": "object",
        "properties": {
            "order_total": {"type": "number", "description": "订单总金额"},
            "customer_level": {"type": "string", "description": "客户等级: bronze/silver/gold"}
        },
        "required": ["order_total", "customer_level"]
    }),
    r#"                          // Rhai 脚本
        let rates = #{
            bronze: 0.05,
            silver: 0.10,
            gold: 0.20,
        };
        let rate = rates[args.customer_level] ?? 0.0;
        let discount = args.order_total * rate;
        #{
            original: args.order_total,
            rate: rate,
            discount: discount,
            final: args.order_total - discount,
        }
    "#,
);

// 注册到 Agent
let agent = AgentBuilder::new("sales")
    .chat_client(client)
    .with_tool(tool)
    .build()?;

创建选项

| 方法 | 说明 | |------|------| | `RhaiTool::new(name, desc, schema, script)` | 标准创建 | | `RhaiTool::with_runtime(name, desc, schema, runtime, script)` | 使用预配置的 Runtime | | `RhaiTool::from_script_file(name, desc, schema, path)` | 从文件加载 `.rhai` 脚本 |

脚本内置变量

| 变量 | 说明 | |------|------| | `args` | Agent 传入的工具参数(JSON 对象) |

完整示例

use rust_agent_rhai::RhaiTool;
use rust_agent_framework::AgentBuilder;
use futures_util::StreamExt;

async fn rhai_augmented_agent() -> anyhow::Result<()> {
    let client = DeepSeekChatClient::new(/* ... */)?;

    // 自定义计算工具(Rhai 脚本)
    let stats_tool = RhaiTool::new(
        "calculate_statistics",
        "计算数据集的基本统计信息",
        serde_json::json!({
            "type": "object",
            "properties": {
                "numbers": {
                    "type": "array",
                    "items": {"type": "number"},
                    "description": "数字数组"
                }
            },
            "required": ["numbers"]
        }),
        r#"
            let nums = args.numbers;
            let count = nums.len();
            if count == 0 {
                return #{error: "数据集为空"};
            }

            let sum = 0.0;
            for n in nums { sum += n; }
            let mean = sum / count;

            // 排序后计算中位数
            nums.sort();
            let median = if count % 2 == 1 {
                nums[count / 2]
            } else {
                (nums[count / 2 - 1] + nums[count / 2]) / 2.0
            };

            // 标准差
            let variance = 0.0;
            for n in nums { variance += (n - mean) * (n - mean); }
            let std_dev = (variance / count).sqrt();

            #{
                count: count,
                sum: sum,
                mean: mean,
                median: median,
                std_dev: std_dev,
                min: nums[0],
                max: nums[count - 1],
            }
        "#,
    );

    let agent = AgentBuilder::new("analyst")
        .chat_client(client)
        .instructions("你是数据分析师。使用 calculate_statistics 处理数据。")
        .with_tool(stats_tool)
        .build()?;

    let input = vec![ChatMessage::user(
        "请分析这组数据的统计特征: [23, 45, 12, 67, 34, 89, 15, 56, 78, 41]"
    )];

    let mut stream = agent.run(input, None, None).await?;
    while let Some(chunk) = stream.next().await {
        if let Ok(result) = chunk {
            for content in &result.contents {
                if let rust_agent_core::Content::Text(ref t) = content {
                    print!("{}", t.delta);
                }
            }
        }
    }

    Ok(())
}

JSON ↔ Dynamic 转换

RhaiRuntime 内置了 serde_json::Value 和 rhai::Dynamic 之间的双向转换:

// JSON → Dynamic
let json = serde_json::json!({"name": "test", "count": 42, "items": [1, 2, 3]});
let dynamic = json_to_dynamic_val(&json);

// Dynamic → JSON
let back = dynamic_to_json_val(&dynamic);
assert_eq!(json, back); // 完美往返

注意事项

1. **性能开销**:Rhai 脚本每次执行都会重新注入变量,对于高频调用的工具建议使用原生 Rust 实现 2. **沙箱限制**:`Engine::new_raw()` 禁用了文件 I/O、网络等危险操作,适合执行不受信任的脚本 3. **错误处理**:脚本编译和执行错误都会被转换为 `AgentError::WorkflowError` 4. **操作限制**:默认 100,000 次操作,可防止无限循环,但复杂脚本可能需要提高此限制 5. **类型安全**:Rhai 是动态类型语言,类型错误在运行时而非编译期发现