系统介绍 llm-chain Rust 库的核心功能和用法,适合想用 Rust 构建 LLM 应用的后端开发者。
LLM 编排是创建 AI 驱动应用的重要组成部分。尤其是在业务用例中,通常会使用 AI 智能体和 RAG 流水线来获得更精准的 LLM 响应。虽然 Langchain 目前是最流行的 LLM 编排套件,但 Rust 中也有一些类似的 crate 可供使用。本文将深入介绍其中之一——llm-chain。
llm-chain 是一组 crate 的集合,自称是使用大语言模型(LLM)创建 AI 驱动应用及其他工具的“终极工具箱”。
你可以在这里找到该 crate 的 GitHub 仓库。
与其他 LLM 编排 crate 的比较#
如果你研究过不同的 crate,可能已经注意到 Rust 中有几个用于 LLM 编排的 crate:
llm-chain(就是本文介绍的这个!)
与其他 crate 相比,llm-chain 对宏的使用相对较多。不过,就额外的数据处理实用工具而言,它也是开发最完善的。如果你正在寻找一个一体化的软件包,llm-chain 最有可能帮助你顺利完成工作。它们也有自己的文档。
在将 llm-chain 添加到项目之前,请确保你能够访问想要使用的提示模型。例如,如果你想使用 OpenAI,请确保拥有 OpenAI API 密钥,并在环境变量中将其设置为 OPENAI_API_KEY。
要开始使用,只需将该 crate 添加到项目中即可(同时添加用于异步处理的 Tokio):
cargo add llm-chain
cargo add tokio -F full
接下来,需要为你想使用的模型提示方式添加一个提供商。这里,我们通过将相应 crate 添加到应用中来引入 OpenAI 集成:
cargo add llm-chain-openai
请注意,完整的集成列表可以在这里找到,并按软件包进行了分类。
为了开始使用 llm-chain,我们可以借助它的基础示例快速运行起来。在以下代码片段中,我们将:
使用 executor!() 初始化一个执行器
使用包含系统消息和提示词的 prompt!(),将两者存储到一个结构体中;运行提示词(或链)时将使用该结构体。
使用执行器的引用运行提示词并返回结果。
use std::error::Error;
use llm_chain::{executor, parameters, prompt};
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let exec = executor!()?;
let res = prompt!(
"You are a robot assistant for making personalized greetings",
"Make a personalized greeting for Joe"
)
.run(¶meters!(), &exec)
.await?;
println!("{}", res);
Ok(())
}
运行这个提示词后,应该会得到类似如下的结果:
Assistant: Hello Joe! I hope you're having a fantastic day filled with joy and success. Remember to keep shining bright and making a positive impact wherever you go. Have a great day!
llm_chain_openai 执行器的默认模型是 gpt-3.5-turbo。执行器参数可以在宏中定义——你也可以在这里了解更多相关信息。
不过,如果想进一步构建更高级的流水线,最简单的方法是使用带参数的提示词模板。如下所示,与前一个代码片段非常相似,我们会生成一个执行器并返回结果。不过,这次并非单独使用 prompt!(),而是将它用于 Step::for_prompt_template——你可以在这里了解更多相关信息。
use llm_chain::step::Step;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let exec = executor!()?;
let step = Step::for_prompt_template(prompt!(
"You are a bot for making personalised greetings",
"Make a personalized greeting tweet for {{text}}"
));
let step_results = step.run(¶meters!("Emil"), &exec).await?;
println!("step_results: {step_results}");
let immediate_results = step_results.to_immediate().await?.as_content();
println!("immediate results: {immediate_results}");
Ok(())
}
输出结果应该类似如下内容:
step_results: Assistant: "Hey @Emil! Wishing you a fantastic day filled with joy, success, and lots of smiles! Keep shining bright and making a positive impact in the world. Cheers to you! 🌟 #YouGotThis"
immediate results: Assistant: "Hey @Emil! Wishing you a fantastic day filled with joy, success, and lots of smiles! Keep shining bright and making a positive impact in the world. Cheers to you! 🌟 #YouGotThis"
当然,我们最初使用 Langchain(或类似 Langchain 的库)的主要原因之一,就是为了编排 LLM 的使用方式。llm-chain Rust crate 允许我们使用 Chain 结构体创建 LLM 提示词链,从而帮助我们实现这一点。
llm-chain 支持三种类型的链:
顺序链,按顺序应用各个步骤
Map-reduce 链,使用“map”步骤处理从已加载文件中切分出的每个块,然后再归约文本。这对于文本摘要非常有用。
对话链,负责跟踪对话历史并管理上下文。对话链非常适合聊天机器人应用、多步骤交互,以及其他上下文至关重要的场景。
最容易使用的链类型是顺序链,它只是将每个步骤的输出通过管道传递给下一个步骤。创建步骤时,我们将使用 Chain 结构体,而不是逐个创建每个步骤:
use llm_chain::step::Step;
use llm_chain::chains::sequential::Chain;
use llm_chain::prompt;
// Create a chain of steps with two prompts
let first_step = Step::for_prompt_template(
prompt!("You are a bot for making personalized greetings", "Make personalized birthday e-mail to the whole company for {{name}} who has their birthday on {{date}}. Include their name")
);
// Second step: summarize the email into a tweet. Importantly, the text parameter is the result of the previous prompt.
let second_step = Step::for_prompt_template(
prompt!( "You are an assistant for managing social media accounts for a company", "Summarize this email into a tweet to be sent by the company, use emojis if you can. \\n--\\n{{text}}")
);
let chain: Chain = Chain::new( vec![first_step, second_step] );
接下来,我们将使用 parameters! 宏向提示词流水线中注入参数:
use llm_chain::parameters;
use llm_chain::traits::Executor as ExecutorTrait;
use llm_chain_openai::chatgpt::Executor;
// Create a new ChatGPT executor with the default settings
let exec = Executor::new()?;
// Run the chain with the provided parameters
let res = chain
.run(
// Create a Parameters object with key-value pairs for the placeholders
parameters!("name" => "Emil", "date" => "February 30th 2023"),
&exec,
)
.await?;
// Print the result to the console
println!("{}", res.to_immediate().await?.as_content());
Ok(())
}
运行代码后,应该会得到类似如下的结果:
Assistant: 🎉🎂 Join us in celebrating Emil's birthday on February 30th! 🎈🎁 Emil, your dedication and hard work are truly commendable. Wishing you happiness and success on your special day! 🥳🎉 #HappyBirthdayEmil #TeamAppreciation 🎂
Map-reduce 链通常由两个步骤组成:
一个“Map”步骤,它接收文档并对其应用 LLM 链,将输出视为一个新文档
随后,将这些新文档传递给一个新的链,由该链合并各个独立文档以获得单一输出。在 Map-Reduce 链的末尾,可以将输出发送给另一个提示模型(例如)进行后续处理,也可以将其作为顺序流水线的一部分。
要使用这种模式,我们需要创建一个提示词模板:
// note that we import Chain from a different module here!
use llm_chain::chains::map_reduce::Chain;
// Create the "map" step to summarize an article into bullet points
let map_prompt = Step::for_prompt_template(prompt!(
"You are a bot for summarizing wikipedia articles, you are terse and focus on accuracy",
"Summarize this article into bullet points:\\n{{text}}"
));
// Create the "reduce" step to combine multiple summaries into one
let reduce_prompt = Step::for_prompt_template(prompt!(
"You are a diligent bot that summarizes text",
"Please combine the articles below into one summary as bullet points:\\n{{text}}"
));
// Create a map-reduce chain with the map and reduce steps
let chain = Chain::new(map_prompt, reduce_prompt);
接下来,需要从文件中读取一些文本并将其添加为参数——Map 提示词中的 {{text}} 参数会自动接收文件内容:
// Load the content of the article to be summarized
let article = include_str!("article_to_summarize.md");
// Create a vector with the Parameters object containing the text of the article
let docs = vec![parameters!(article)];
let exec = executor!()?;
// 使用提供的文档和一个空的 Parameters 对象运行链,用于 "reduce" 步骤
// 请注意,多个模块中都存在 Chain 结构体
// 这里的 Chain 接收两个不同的模块
let res = chain.run(docs, Parameters::new(), &exec).await?;
// 将结果打印到控制台
println!("{}", res.to_immediate().await?.as_content());
请注意,由于这里有两个步骤,chain.run() 函数会按顺序接收两个不同的向量——每个步骤对应一个。这意味着我们会将文章内容传递给第一个提示词,但不会向第二个文档传递任何参数。
当然,我们最后需要讨论的是对话链。简而言之,对话链允许你使用已保存的聊天历史记录,从内存中加载上下文。在平台或模型无法访问已保存聊天历史记录的情况下,你可以存储响应,然后在下一条消息中将其用作额外的上下文。
要使用对话链,与之前一样,我们需要创建一个 Chain(这次从 conversation 模块导入),并为其定义步骤:
use llm_chain::{
chains::conversation::Chain, executor, output::Output, parameters, prompt, step::Step,
};
let exec = executor!()?;
let mut chain = Chain::new(
prompt!(system: "You are a robot assistant for making personalized greetings."),
)?;
// Define the conversation steps.
let step1 = Step::for_prompt_template(prompt!(user: "Make a personalized greeting for Joe."));
let step2 =
Step::for_prompt_template(prompt!(user: "Now, create a personalized greeting for Jane."));
let step3 = Step::for_prompt_template(
prompt!(user: "Finally, create a personalized greeting for Alice."),
);
let step4 = Step::for_prompt_template(prompt!(user: "Remind me who did we just greet."));
接下来,我们会依次将每个提示词分别发送给 Chain,并打印每个提示词对应的响应。请注意,在第 4 步中,我们应该会收到一个包含之前刚刚为其创建个性化问候语的三个人姓名(Joe、Jane 和 Alice)的回答。
// Execute the conversation steps.
let res1 = chain.send_message(step1, ¶meters!(), &exec).await?;
println!("Step 1: {}", res1.to_immediate().await?.primary_textual_output().unwrap());
let res2 = chain.send_message(step2, ¶meters!(), &exec).await?;
println!("Step 2: {}", res2.to_immediate().await?.primary_textual_output().unwrap());
let res3 = chain.send_message(step3, ¶meters!(), &exec).await?;
println!("Step 3: {}", res3.to_immediate().await?.primary_textual_output().unwrap());
let res4 = chain.send_message(step4, ¶meters!(), &exec).await?;
println!("Step 4: {}", res4.to_immediate().await?.primary_textual_output().unwrap());
运行后应该会得到类似下面的输出:
Step 1: Hello, Joe! I hope you are having a fantastic day filled with positivity and joy. Keep shining bright and making a difference in the world with your unique presence. Wishing you continued success and happiness in all that you do!
Step 2: Hello, Jane! Sending you warm greetings and positive vibes today. May your day be as wonderful and vibrant as you are. Remember to keep being your amazing self and always believe in the incredible things you are capable of achieving. Wishing you endless happiness and success in all your endeavors!
Step 3: Hello, Alice! I hope this message finds you well and thriving. You are such a remarkable individual with a heart full of kindness and a spirit full of strength. Keep inspiring those around you with your grace and resilience. May your day be filled with love, laughter, and countless blessings. Stay amazing, Alice!
Step 4: We just created personalized greetings for Joe, Jane, and Alice.
加入使用 Shuttle 进行构建的开发者行列
我已经使用 Shuttle 部署了自己的第二个服务,真的很喜欢它!它速度很快,并且与 cargo 集成良好,因此我可以专注于 Rust 代码,而不必操心部署。做得很棒!
在使用 llm-chain 处理嵌入方面,它提供了一个辅助方法,可将 Qdrant 用作向量存储。该方法对 qdrant_client crate 进行了抽象,提供了一种简单的方式来嵌入文档并执行相似度搜索。请注意,Qdrant 结构体会假定你想使用的集合已经创建完毕!
虽然我们可以使用 qdrant_client 手动创建自己的嵌入,但 llm-chain 也提供了便于使用嵌入的集成。我们需要通过 qdrant_client 创建自己的客户端,然后将其与 Qdrant 结构体配合使用,以便解析内容。
首先,让我们定义几个想要插入 Qdrant 集合的文本段落:
const DOC_DOG_DEF: &str = r#"The dog (Canis familiaris[4][5] or Canis lupus familiaris[5]) is a domesticated descendant of the wolf. Also called the domestic dog, it is derived from the extinct Pleistocene wolf,[6][7] and the modern wolf is the dog's nearest living relative.[8] Dogs were the first species to be domesticated[9][8] by hunter-gatherers over 15,000 years ago[7] before the development of agriculture.[1] Due to their long association with humans, dogs have expanded to a large number of domestic individuals[10] and gained the ability to thrive on a starch-rich diet that would be inadequate for other canids.[11]
The dog has been selectively bred over millennia for various behaviors, sensory capabilities, and physical attributes.[12] Dog breeds vary widely in shape, size, and color. They perform many roles for humans, such as hunting, herding, pulling loads, protection, assisting police and the military, companionship, therapy, and aiding disabled people. Over the millennia, dogs became uniquely adapted to human behavior, and the human-canine bond has been a topic of frequent study.[13] This influence on human society has given them the sobriquet of "man's best friend"."#;
const DOC_WOODSTOCK_SOUND: &str = r#"Sound for the concert was engineered by sound engineer Bill Hanley. "It worked very well", he says of the event. "I built special speaker columns on the hills and had 16 loudspeaker arrays in a square platform going up to the hill on 70-foot [21 m] towers. We set it up for 150,000 to 200,000 people. Of course, 500,000 showed up."[48] ALTEC designed marine plywood cabinets that weighed half a ton apiece and stood 6 feet (1.8 m) tall, almost 4 feet (1.2 m) deep, and 3 feet (0.91 m) wide. Each of these enclosures carried four 15-inch (380 mm) JBL D140 loudspeakers. The tweeters consisted of 4x2-Cell & 2x10-Cell Altec Horns. Behind the stage were three transformers providing 2,000 amperes of current to power the amplification setup.[49][page needed] For many years this system was collectively referred to as the Woodstock Bins.[50] The live performances were captured on two 8-track Scully recorders in a tractor trailer back stage by Edwin Kramer and Lee Osbourne on 1-inch Scotch recording tape at 15 ips, then mixed at the Record Plant studio in New York.[51]"#;
Qdrant 结构体会自动假定你的集合已经设置完毕、已有一个现成的 QdrantClient,并且已有集合名称。我们会将这些内容作为参数传入一个新函数,该函数会执行以下操作:
llm-chain-openai 创建嵌入首先,我们需要定义一个用于创建 Qdrant 结构体的方法,以便稍后重复使用:
use llm_chain_openai::embeddings::Embeddings;
use llm_chain_qdrant::Qdrant;
use llm_chain::schema::EmptyMetadata;
use qdrant_client::prelude::{QdrantClient, QdrantClientConfig};
// note that the URL must connect to port 6334 - qdrant_client uses GRPC!
// feel free to replace this wit
async fn create_qdrant_client(url: String) -> QdrantClient {
let mut config = QdrantClientConfig::from_url(url);
// this part is only required if you're running Qdrant on the cloud
// if running locally, no api key is required
config.api_key = std::env::var("QDRANT_API_KEY").ok();
QdrantClient::new(Some(config))
}
async fn create_qdrant_struct(qdrant_client: QdrantClient, collection_name: String) -> Qdrant {
let embeddings = Embeddings::default();
// Storing documents
Qdrant::new(
qdrant_client,
collection_name,
embeddings,
None,
None,
None,
)
}
接下来,我们可以使用 Qdrant 结构体执行相似度搜索!我们会先将文档添加到集合中,然后执行相似度搜索,并打印存储的文档:
async fn similarity_search_qdrant(qdrant: Qdrant) -> Result<(), Box<dyn Error>> {
// embed and upsert the documents into Qdrant
let doc_ids = qdrant
.add_documents(
vec![
DOC_DOG_DEF.to_owned(),
DOC_WOODSTOCK_SOUND.to_owned(),
]
.into_iter()
.map(Document::new)
.collect(),
)
.await?;
println!("Documents stored under IDs: {:?}", doc_ids);
// conduct similarity search and find similar vectors
let response = qdrant
.similarity_search(
"Sound engineering is involved with concerts and music events".to_string(),
1,
)
.await?;
// print out the stored documents with payload, embeddings etc
println!("Retrieved stored documents: {:?}", response);
}
完成这些操作后,我们便可以将结果传入 Chain,或者用于其他任何需要的地方。
单独使用时,Qdrant 结构体并没有太大帮助,它主要提供了一些便于生成嵌入的方法。不过,我们也可以将它添加为 ToolCollection 的一部分,从而让流水线知道自己能够使用嵌入。
let qdrant = create_qdrant_struct( ,
"mycollection".to_string()).await;
let exec = executor!().unwrap();
let mut tool_collection = ToolCollection::<Multitool>::new();
tool_collection.add_tool(
QdrantTool::new(
qdrant,
"factual information and trivia",
"facts, news, knowledgebuild_local_qdrant().await; or trivia",
)
.into(),
);
let task = "Tell me something about dogs";
let prompt = ChatMessageCollection::new()
.with_system(StringTemplate::tera(
"You are an automated agent for performing tasks. Your output must always be YAML.",
))
.with_user(StringTemplate::combine(vec![
tool_collection.to_prompt_template().unwrap(),
StringTemplate::tera("Please perform the following task: {{task}}."),
]));
let result = Step::for_prompt_template(prompt.into())
.run(¶meters!("task" => task), &exec)
.await
.unwrap();
println!("{}", res.to_immediate().await?.as_content());
虽然 llm-chain 提供了用于创建 LLM 流水线的工具,但 Langchain 及其他类似库的另一个重要组成部分,是处理和转换数据的能力。正确设计提示词(以及做好提示词工程)非常重要。然而,如果我们还要向流水线中输入数据,就也需要确保能够尽可能轻松地找到最相关的上下文。
下面是几个值得了解的实用场景。
llm-chain 提供了一个便捷的 GoogleSerper 结构体,可通过 Serper.dev 服务抓取 Google 搜索结果。
use llm_chain::tools::{tools::GoogleSerper, Tool};
#[tokio::main(flavor = "current_thread")]
async fn main() {
let serper_api_key = std::env::var("SERPER_API_KEY").unwrap();
let serper = GoogleSerper::new(serper_api_key);
let result = serper
.invoke_typed(&"Who was the inventor of Catan?".into())
.await
.unwrap();
println!("Best answer from Google Serper: {}", result.result);
}
除此之外,它还支持 Bing Search API,该 API 每月提供 1000 次免费搜索。下面的代码片段展示了如何使用该 API:
use llm_chain::tools::{tools::BingSearch, Tool};
#[tokio::main(flavor = "current_thread")]
async fn main() {
let bing_api_key = std::env::var("BING_API_KEY").unwrap();
let bing = BingSearch::new(bing_api_key);
let result = bing
.invoke_typed(&"Who was the inventor of Catan?".into())
.await
.unwrap();
println!("Best answer from bing: {}", result.result);
}
如果需要在两者之间切换,它们使用起来都相当简单。
llm-chain 还提供了一些用于提取带标签文本的便捷方法。例如,如果你有一个由项目符号组成的字符串,就可以使用 extract_labeled_text() 提取其中的文本。
use llm_chain::parsing::extract_labeled_text;
fn main() {
let text = r"
- Title: The Matrix
- Actor: Keanu Reeves
- Director: The Wachowskis
";
let result = extract_labeled_text(text);
println!("{:?}", result);
}
运行这段代码后,应该会得到如下所示的输出:
[("Title", "The Matrix"), ("Actor", "Keanu Reeves"), ("Director", "The Wachowskis")]
你还可以进一步了解 llm-chain 的解析模块及其部分示例。
感谢阅读!借助 llm-chain 的强大能力,你可以轻松地在应用程序中利用 AI。
构建 RAG 智能体工作流
在 Rust 中使用 Huggingface
构建一个与 llm-chain 配合使用的 Axum Web 服务器