SmallThinker-3B-Preview代码实例:Rust调用Ollama API构建低延迟COT命令行工具

1. 引言:为什么需要自己的COT推理工具?

如果你尝试过在本地运行大模型进行复杂的推理任务,比如让模型一步步思考数学问题、分析逻辑链条或者生成长篇的思考过程,你可能会遇到两个痛点:一是Web界面交互不够灵活,二是通过HTTP请求调用时,总觉得响应不够快,尤其是在处理长链思维(Chain-of-Thought, COT)推理时。

今天,我们就来解决这个问题。我将带你用Rust语言,基于强大的SmallThinker-3B-Preview模型和Ollama的API,亲手打造一个属于你自己的、低延迟的COT推理命令行工具。这个工具能让你在终端里,像使用lsgrep命令一样,快速调用模型进行深度思考。

你能学到什么?

  • 快速理解SmallThinker-3B-Preview模型的特点和适用场景。
  • 掌握如何通过Rust调用Ollama的API。
  • 一步步构建一个功能完整、响应迅速的命令行交互工具。
  • 获得可立即运行、并可根据自己需求修改的完整代码。

你需要准备什么?

  • 一台已经安装并运行了Ollama的电脑(Ollama的安装非常简单,官网有详细指南)。
  • 在Ollama中拉取并运行了smallthinker:3b模型(命令:ollama run smallthinker:3b)。
  • 一个代码编辑器(如VSCode)和Rust开发环境(通过rustup安装)。

准备好了吗?让我们开始吧。

2. 认识我们的核心:SmallThinker-3B-Preview

在动手写代码之前,我们先花几分钟了解一下我们将要驱动的“引擎”。

2.1 它从何而来?

SmallThinker-3B-Preview并不是一个从零开始训练的模型。它的起点是Qwen2.5-3b-Instruct,一个在中文社区广受好评的、能力均衡的3B参数模型。开发者在这个优秀的基础上,进行了针对性的微调

你可以把微调想象成:一个已经学会了通用知识(如语言、逻辑)的“通才”,经过一段时间的“专项特训”,变成了某个领域的“专家”。SmallThinker的特训方向,就是长链思维推理

2.2 它被设计用来做什么?

这个模型的定位非常清晰,主要瞄准两个核心场景:

  1. 边缘部署:它的“身材”很苗条(3B参数),这意味着它不需要昂贵的GPU或大量的内存就能跑起来。你可以把它部署在树莓派、老旧笔记本甚至一些嵌入式设备上,让AI推理能力延伸到网络的边缘。
  2. 充当“草稿员”:在一个更复杂的系统中,比如配合一个更大的模型(如文中提到的QwQ-32B-Preview),SmallThinker可以扮演“快速打草稿”的角色。先由它快速生成一个推理的草稿或初稿,再由大模型进行精修和确认。这种协作模式,据说能将整体推理速度提升高达70%。

2.3 它的“特训”秘籍:QWQ-LONGCOT-500K数据集

模型能力强的背后,是高质量的数据。为了让SmallThinker擅长长链推理,开发者创建了一个名为QWQ-LONGCOT-500K的专用数据集。

这个数据集有什么特别之处?

  • 超长输出:里面超过75%的样本,其输出内容的长度(令牌数)都超过了8K。这意味着模型在学习如何生成非常冗长、细致的推理步骤。
  • 合成技术:数据是通过各种合成技术(例如personahub)生成的,这能创造出多样化和复杂的推理场景。
  • 开源共享:最重要的是,这个数据集是公开的!这鼓励了整个开源社区基于此进行进一步的研究和改进。

简单来说,SmallThinker是一个为“一步一步慢慢想”而生的、小巧但专精的模型。接下来,我们就用Rust给它打造一个高效的“方向盘”和“油门踏板”。

3. 项目搭建与核心依赖

我们的工具将是一个标准的Rust命令行项目。打开你的终端,开始第一步。

3.1 创建新项目

cargo new smallthinker-cli
cd smallthinker-cli

这条命令会创建一个名为smallthinker-cli的新目录,里面包含了基本的Rust项目文件。

3.2 编辑 Cargo.toml 添加依赖

打开项目根目录下的Cargo.toml文件,在[dependencies]部分添加以下内容:

[dependencies]
reqwest = { version = "0.12", features = ["json", "blocking"] } # 用于发送HTTP请求到Ollama
tokio = { version = "1.0", features = ["full"] } # Rust的异步运行时,让我们的程序可以高效地等待网络响应
serde = { version = "1.0", features = ["derive"] } # 用于序列化和反序列化JSON数据
serde_json = "1.0" # 处理JSON格式
clap = { version = "4.0", features = ["derive"] } # 一个强大的命令行参数解析库,让我们轻松定义命令格式
colored = "2.1" # 让终端输出有颜色,更美观

这些依赖各自的作用

  • reqwest:是我们的“网络信使”,负责向Ollama服务(默认在http://localhost:11434)发送请求并获取回复。
  • tokio:因为网络请求是“异步”操作(需要等待),tokio提供了管理这些等待任务的“后台调度器”,让程序不会卡住。
  • serde & serde_json:Ollama API接收和返回的数据都是JSON格式。这两个库帮我们在Rust的结构体(struct)和JSON字符串之间轻松转换。
  • clap:用来解析你在命令行输入的参数,比如-p “你的问题”
  • colored:给输出文字加点颜色,区分用户输入和模型回复,提升可读性。

保存文件后,在项目目录下运行 cargo build,Cargo包管理器会自动下载这些依赖。

4. 理解Ollama API:我们如何与模型对话?

Ollama提供了一个非常简洁的REST API。我们主要使用它的 /api/generate 端点来让模型生成文本。

4.1 API请求格式

我们需要向 http://localhost:11434/api/generate 发送一个POST请求,请求体是一个JSON对象,其中最重要的两个字段是:

  • model: 字符串,指定使用哪个模型,这里我们填 "smallthinker:3b"
  • prompt: 字符串,即我们给模型的输入指令或问题。
  • stream (可选): 布尔值。如果设为 true,API会以流式(一段一段)返回结果,适合实时显示。我们第一个版本为了简单,先设为 false,一次性获取完整回复。

一个最简单的请求体看起来像这样:

{
  "model": "smallthinker:3b",
  "prompt": "请一步步推理:一个篮子里有5个苹果,我拿走了2个,又放进去3个,现在篮子里有几个苹果?",
  "stream": false
}

4.2 API响应格式

stream: false时,API会返回一个完整的JSON响应,其中我们最关心的字段是:

  • response: 字符串,模型生成的完整回复内容。

响应示例:

{
  "model": "smallthinker:3b",
  "created_at": "2024-01-01T00:00:00.000000Z",
  "response": "让我们一步步思考:\n1. 最初,篮子里有5个苹果。\n2. 拿走了2个,所以剩下 5 - 2 = 3个苹果。\n3. 然后又放进去3个,现在总数为 3 + 3 = 6个苹果。\n\n因此,现在篮子里有6个苹果。",
  "done": true
}

了解了通信协议,我们就可以用Rust定义对应的数据结构了。

5. 编写代码:从定义数据结构开始

src/main.rs 文件中,我们将逐步编写所有代码。首先,定义与Ollama API交互所需的数据结构。

5.1 定义请求和响应结构体

use serde::{Deserialize, Serialize};

// 这是我们发送给Ollama的请求体
#[derive(Serialize)]
struct GenerateRequest {
    model: String,
    prompt: String,
    stream: bool,
}

// 这是我们从Ollama接收的响应体(非流式模式)
#[derive(Deserialize)]
struct GenerateResponse {
    response: String,
    // 实际响应中还有其他字段,但我们只关心`response`
}

Serialize trait 让Rust能把 GenerateRequest 结构体转换成JSON字符串。Deserialize trait 让Rust能把接收到的JSON字符串解析成 GenerateResponse 结构体。

5.2 使用Clap定义命令行参数

我们希望工具这样使用:smallthinker-cli -p “你的问题”。用Clap可以非常优雅地实现。

use clap::Parser;

/// 一个用于与SmallThinker-3B模型交互的低延迟命令行工具
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// 提供给模型的提示词或问题
    #[arg(short, long)]
    prompt: String,

    /// Ollama服务的地址,默认为 http://localhost:11434
    #[arg(short, long, default_value = "http://localhost:11434")]
    ollama_host: String,
}

#[derive(Parser)] 让Clap自动为 Cli 结构体生成参数解析逻辑。#[arg] 属性用来定义每个字段对应的命令行参数。

  • prompt: -p--prompt 后面跟的问题字符串。
  • ollama_host: -o--ollama-host,允许用户自定义Ollama地址,默认就是本地。

6. 核心功能:实现模型调用函数

现在我们来编写最核心的部分——一个函数,它接收一个提示词,调用Ollama API,并返回模型的回复。

6.1 创建异步请求函数

我们将使用 reqwesttokio 来执行异步HTTP请求。

use reqwest::Client;
use colored::*;

async fn ask_model(host: &str, prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
    // 1. 创建HTTP客户端
    let client = Client::new();
    // 2. 构建API请求的完整URL
    let url = format!("{}/api/generate", host);
    
    // 3. 构建请求体
    let request_body = GenerateRequest {
        model: "smallthinker:3b".to_string(),
        prompt: prompt.to_string(),
        stream: false, // 首次实现,使用非流式
    };

    println!("{}", "正在思考...".yellow().italic());

    // 4. 发送POST请求并等待响应
    let response = client
        .post(&url)
        .json(&request_body) // 自动将结构体序列化为JSON
        .send()
        .await?; // `await`等待请求完成,`?`在出错时提前返回错误

    // 5. 检查HTTP响应状态是否成功
    if !response.status().is_success() {
        let status = response.status();
        let error_text = response.text().await.unwrap_or_default();
        return Err(format!("Ollama API 错误 ({}): {}", status, error_text).into());
    }

    // 6. 将响应的JSON解析为我们的 GenerateResponse 结构体
    let api_response: GenerateResponse = response.json().await?;

    // 7. 返回模型生成的内容
    Ok(api_response.response)
}

代码逐行解读

  1. 创建一个可复用的HTTP客户端。
  2. 拼接出完整的API地址。
  3. 用我们之前定义的结构体构造请求数据。
  4. client.post(...).json(...).send().await? 是链式调用,发起请求并等待结果。这是典型的异步编程模式。
  5. 检查HTTP状态码,如果不是成功(2xx),则读取错误信息并返回。
  6. 将成功的响应体解析为 GenerateResponse 结构体。
  7. 提取并返回其中的 response 字段。

这个函数是异步的(async fn),这意味着它内部可以包含等待网络IO的操作而不阻塞线程。

7. 组装主函数:让一切运转起来

最后,我们需要一个 main 函数来串联所有部分:解析参数、调用模型、输出结果。

#[tokio::main] // 这个属性宏将普通的main函数转换为Tokio异步运行时的主函数
async fn main() {
    // 1. 解析命令行参数
    let args = Cli::parse();

    // 2. 打印用户输入的问题,方便查看
    println!("{}", "你的问题:".green().bold());
    println!("{}\n", args.prompt);
    println!("{}", "-".repeat(50).dimmed());

    // 3. 调用模型,并处理可能发生的错误
    match ask_model(&args.ollama_host, &args.prompt).await {
        Ok(answer) => {
            // 成功获取回复
            println!("{}", "SmallThinker 的推理:".cyan().bold());
            println!("{}", answer);
        }
        Err(e) => {
            // 如果出错,打印错误信息
            eprintln!("{}: {}", "错误".red().bold(), e);
            std::process::exit(1); // 以非零状态码退出,表示程序异常结束
        }
    }
}

代码逻辑

  1. Cli::parse() 会自动处理命令行输入,并填充到 args 变量中。
  2. 用绿色加粗文字打印用户的问题,并用一条灰色的分隔线增加可读性。
  3. 使用 match 语句处理 ask_model 函数的结果。
    • Ok(answer):调用成功,用青色加粗文字打印标题,然后输出模型的回复。
    • Err(e):调用失败,用红色加粗文字打印错误信息,并退出程序。

8. 运行与测试:看看效果如何

代码已经完成!让我们来测试一下。

8.1 编译并运行

在项目根目录下,运行:

cargo run -- -p “请用链式思维(COT)解释一下,为什么天空是蓝色的?”

cargo run 会自动编译并运行程序。-- 后面的部分会传递给我们的程序。-p 后面跟着的就是我们的问题。

你应该会看到类似这样的输出:

你的问题:
请用链式思维(COT)解释一下,为什么天空是蓝色的?

--------------------------------------------------
正在思考...
SmallThinker 的推理:
好的,让我们一步步推理(Chain-of-Thought)为什么天空是蓝色的。

1. **光源**:首先,我们需要考虑光的来源。白天天空的光主要来自太阳。
2. **光的本质**:太阳光看起来是白色的,但实际上它是由不同颜色的光混合而成的,这些颜色对应不同的波长(彩虹的颜色:红、橙、黄、绿、蓝、靛、紫)。
3. **大气层的作用**:地球被一层大气包围。大气中含有大量的气体分子(主要是氮气和氧气)以及微小的尘埃、水滴等粒子。
4. **瑞利散射**:当太阳光进入大气层时,会与这些微小的气体分子发生相互作用。一种叫做“瑞利散射”的物理现象在这里起关键作用。瑞利散射的强度与光波长的四次方成反比(I ∝ 1/λ⁴)。
5. **波长比较**:这意味着波长越短的光,被散射得越强烈。在可见光中,蓝色和紫色光的波长最短(大约450-495纳米),红色光的波长最长(大约620-750纳米)。
6. **散射效果**:因此,蓝色光比红色光被大气分子散射的程度要强得多(大约强出(700/450)⁴ ≈ 10倍)。
7. **我们看到的天空**:这些被强烈散射的蓝色光向四面八方散开,充满了整个大气层。当我们仰望天空时(除了直接看太阳的方向),我们看到的正是这些被散射到我们眼睛里的蓝色光。
8. **为什么不是紫色?** 紫色光波长更短,散射其实更强。但我们看到的天空主要是蓝色,而不是紫色,原因有两个:一是太阳光谱中蓝色光比紫色光能量更强;二是人眼对蓝色比紫色更敏感。

**结论**:所以,天空呈现蓝色,主要是因为太阳光中的短波长蓝色光在大气中发生了强烈的瑞利散射,这些散射光从各个方向进入我们的眼睛,形成了蓝色的天空。

看!SmallThinker成功地运用了COT推理,给出了一个逻辑清晰、步骤分明的解释。我们的命令行工具工作正常!

8.2 尝试更多问题

你可以尝试不同需要推理的问题:

# 数学问题
cargo run -- -p “如果3个人3天能喝3桶水,那么9个人9天能喝多少桶水?请一步步推理。”

# 逻辑推理
cargo run -- -p “假设:所有猫都怕水。我的宠物汤姆怕水。那么汤姆一定是猫吗?请分析这个推理是否有效。”

# 创意写作(也能体现推理结构)
cargo run -- -p “为一个科幻短篇小说构思一个开头。要求:1. 包含一个意外的发现;2. 设置一个悬念。请先列出构思要点,再写出段落。”

9. 总结与进阶思考

恭喜你!你已经成功构建了一个基于SmallThinker-3B-Preview和Rust的低延迟COT推理命令行工具。让我们回顾一下并看看未来可以如何改进。

9.1 我们完成了什么?

  1. 项目初始化:创建了Rust项目,并引入了必要的依赖(网络请求、JSON解析、命令行参数处理)。
  2. 理解API:明确了如何通过HTTP JSON与Ollama服务中的模型进行交互。
  3. 定义数据结构:用serde库定义了请求和响应的Rust结构体,使数据转换变得安全简单。
  4. 实现核心逻辑:编写了ask_model异步函数,负责与Ollama通信并处理错误。
  5. 构建用户界面:使用clap库定义了简洁的命令行参数,使用colored库美化了输出。
  6. 集成测试:成功运行工具,并验证了SmallThinker模型出色的链式思维推理能力。

9.2 这个工具的优势

  • 低延迟:Rust语言的高性能加上直接HTTP调用,避免了Web界面的开销,响应迅速。
  • 易于集成:命令行工具可以轻松嵌入到脚本、自动化流程或其他程序中。
  • 配置灵活:通过参数可以轻松指定不同的Ollama服务地址。
  • 代码清晰:结构良好的Rust代码易于维护和扩展。

9.3 可能的改进方向

我们的第一个版本已经可用,但还有很大的增强空间:

  1. 支持流式响应:将stream设为true,然后处理服务器发送的(Server-Sent Events, SSE)数据流,实现打字机式的逐字输出效果,体验更佳。
  2. 添加对话历史:修改程序,使其能记住上下文,实现多轮对话,而不仅仅是单次问答。
  3. 可配置模型:通过命令行参数(如-m)让用户可以指定使用不同的Ollama模型,而不仅仅是smallthinker:3b
  4. 调整生成参数:在请求体中加入temperature(控制随机性)、top_p(核采样)等参数,让用户能控制生成文本的“创造性”或“确定性”。
  5. 更丰富的错误处理:区分网络错误、模型加载错误、输入错误等,给出更友好的提示。
  6. 构建与分发:使用cargo build --release编译发布版本,并分享给其他用户使用。

这个工具是你探索本地大模型应用的一个绝佳起点。你可以基于此,将它改造成你自己的AI助手、代码审查工具、学习伙伴,或者任何你能想到的创意应用。Rust的安全性和性能,加上SmallThinker专精的推理能力,一定能碰撞出更多火花。


获取更多AI镜像

想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐