用 Rust 构建 Agent 应用的高性能框架:langchainrust 架构全景

用 Rust 构建 Agent 应用的高性能框架:langchainrust 架构全景

一个 LLM Agent 应用要用到的东西很多:模型接入、提示词、链式组合、状态机编排、工具调用、检索增强、多轮记忆、护栏、评测、可观测,还要跟 MCP 工具、别的 Agent 互联。langchainrust 用 21 个 crate、5 层架构把它们组织成一套完整运行时。本文从地基拆到屋顶,每个设计点都附真实源码与取舍。


问题场景:LLM 应用工程化,难的不是「调模型」

写一个能用的 LLM 应用很简单------调一个 chat 接口就完了。写一个能上线、能迭代、能出错的 LLM 应用,是另一回事:

真实需求 对应系统
一个应用要换 5 家模型 供应商抽象
提示词 + 模型 + 解析器串起来 组合层
模型自己决定调哪个工具 Agent 循环
多步骤工作流、条件分支、循环 状态机编排
问答要带自己的文档 RAG
多轮对话要记住上下文 记忆
Agent 调的工具来自外部进程 MCP
多个 Agent 互相派活 A2A
不让模型输出泄露密钥 护栏
换模型后质量掉了没 评测
上线后出了事故怎么查 可观测

很多生态的答案是把这些拆成几十个互相依赖的包。langchainrust 的答案是:一个 facade、五层架构、21 个 crate 。它回答的是「这个框架是干嘛的」------LLM Agent 应用的一套完整运行时,不是某个单点的工具。


为什么是 Rust?

LangChain 这类框架起家于动态语言生态。用 Rust 重写,不是跟风,是冲着四个工程问题去的:

动态语言生态的工程问题 Rust 的回应
弱类型:字段写错、None 冒出来,运行时才炸 类型系统把大部分错误挡在编译期
全局解释器锁:多线程模型推理并不同时跑 无锁并发,多 worker 真并行
GC 停顿:延迟抖动不可控 无 GC,确定性释放
解释器 + 依赖地狱:冷启动慢、部署重 单二进制,cargo build --release 一个文件拷走

但对框架作者来说,Rust 有个更贴合 LLM 编排的特性------零成本抽象。同样是「把提示词、模型、解析器串成一条链」,动态语言靠运行时的 dict 传来传去、鸭子类型兜底;Rust 可以在编译期把链的类型算清楚,还能在该擦除的地方擦除(后面「高性能从哪来」一节细讲)。


整体架构:一个 facade,五层,21 个 crate

仓库是 21-crate 的 workspace。用户只依赖门面 langchainrust(即 crates/lc),它把全部公共 API 再导出。依赖只往下走------lc-shared / lc-schema 垫底,被所有人依赖,环依赖问题就这样从结构上消掉。

复制代码
                        langchainrust  (facade, crates/lc)
        ┌──────────────────────┼───────────────────────┐
   基础层 Foundation    能力层 Capabilities    组合层 Composition
   lc-shared           lc-providers          lc-chains
   lc-schema           lc-embeddings         lc-langgraph
   lc-core             lc-prompts            lc-agents
                       lc-tools              lc-rag
                       lc-tools-derive       lc-vector-stores
                                             lc-memory
                                             lc-sessions
        ┌──────────────────────┼───────────────────────┐
   互联层 Protocol     质量层 Quality
   lc-mcp              lc-guardrails
   lc-a2a              lc-evaluation
                       lc-callbacks
crate 回答的问题
基础层 lc-shared lc-schema lc-core 类型地基:Message / Document / Runnable / BaseChatModel / BaseTool
能力层 lc-providers lc-embeddings lc-prompts lc-tools lc-tools-derive 模型、向量、提示词、工具,每个能力一个抽象
组合层 lc-chains lc-langgraph lc-agents lc-rag lc-vector-stores lc-memory lc-sessions 把能力编排成应用:链、状态图、Agent、RAG、记忆
互联层 lc-mcp lc-a2a 跟外部世界打交道:工具协议、Agent 间协议
质量层 lc-guardrails lc-evaluation lc-callbacks 上线前的护栏、评测,上线后的可观测

下面逐层拆,每层拿一个真实组件看它是怎么设计的。


五层逐一拆解

基础层:类型地基

最底下的三个 crate 定义了整个框架的「字」------所有上层都建立在它们之上:

  • lc-schema :Message(角色 + 内容 + 多模态附件)。LLM 应用最核心的数据结构,就一个类型。
  • lc-shared :跨 crate 的基础类型 Document / VectorDocument / SearchResult / ToolCall / TextSplitter。把「文档」这种东西定义在最底层,上层随便用。
  • lc-core :执行层。最关键的抽象是 Runnable------LCEL 的组合子,四个基础动作 invoke / batch / stream / transform。它定义了「一个可执行的东西长什么样」。

模型的统一入口也在这一层,BaseChatModel 一个 trait 概括所有模型供应商:

rust 复制代码
#[async_trait]
pub trait BaseChatModel: BaseLanguageModel<Vec<Message>, LLMResult> {
    async fn chat(
        &self,
        messages: Vec<Message>,
        config: Option<RunnableConfig>,
    ) -> Result<LLMResult, Self::Error>;

    async fn stream_chat(
        &self,
        messages: Vec<Message>,
        config: Option<RunnableConfig>,
    ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error>;
}

注意两件事:返回类型里的 Self::Error------每家供应商的关联错误类型不同 ,这正是异构模型池要靠类型擦除来统一的根源(见后面 RouterLLM 的 ModelAdapter);而 stream_chat 返回 + Send 的流、方法本身是 async------任何实现者都能被扔进多线程运行时并发调用,没有 GIL 卡着。

能力层:模型、向量、提示词、工具

这一层每个能力一个抽象、一堆实现,把「换供应商」变成「改一个构造器」:

rust 复制代码
let deepseek = DeepSeekChat::from_env();
let moonshot = MoonshotChat::with_model("moonshot-v1-128k");
let claude = AnthropicChat::from_env();
let ollama = OllamaChat::new("llama3.2");

lc-providers 一家供应商一个 crate 内部模块,但都实现了 BaseChatModel------应用代码从不多写一个 match。OpenAI 系的小厂(DeepSeek / Qwen / Moonshot / Zhipu / Mistral)复用 OpenAI 的请求路径,只保留各自的错误变体(ProviderError::DeepSeek),新供应商接入成本被压到极低。

提示词在 lc-prompts,PromptTemplate 解析一次、缓存分段------不是每次调用都重 parse 。工具在 lc-tools,最有料的是 #[tool] 宏:一个普通函数标一下,自动变成 BaseTool:

rust 复制代码
#[tool(description = "Greets a person by name")]
fn greet(
    #[param(desc = "The name of the person to greet")] name: String,
) -> Result<String, ToolError> {
    Ok(format!("Hello, {}!", name))
}

宏自动展开出一个实现 BaseTool 的类型(测试里直接 GreetTool::new()),工具名、参数 schema、JSON 序列化全部生成------写工具像写普通函数,Agent 调用的工具却带上了完整类型信息,不需要手写工具 schema。

组合层:链、图、智能体、RAG、记忆

这层是「全家桶」的核心,也是代码最厚的地方。三个编排范式,由简到繁:

一、LCEL 链 ------Runnablepipe 把一切串起来。v0.15 之后,提示词、记忆、原生模型、解析器、RAG 全是 Runnable,五样东西能 pipe 成一条链:

rust 复制代码
// 提示词 + 模型 + 解析器
let prompt = ChatPromptTemplate::from_messages([
    Message::system("你是一个简洁的 Rust 助手,只输出结论。"),
    Message::human("{question}"),
]);
let qa_chain = prompt.pipe(llm.clone()).pipe(StrOutputParser::new());

// 记忆 + 模型 + 解析器(读记忆 → 拼输入 → 生成 → 写回,封装在一个 Runnable 里)
let memory = ConversationBufferMemory::new().with_return_messages(true);
let chat_chain = RunnableWithMessageHistory::new(llm.clone(), memory)
    .pipe(StrOutputParser::new());

// RAG:检索增强生成作为链的一段
let pipeline = RAGPipelineBuilder::new()
    .llm(llm)
    .retriever(retriever)
    .retrieve_k(2)
    .build()?;
let rag_chain = RagRunnable::new(Arc::new(pipeline));

pipe 的类型是编译器算出来的------prompt.pipe(llm) 返回什么类型、能不能接着 .pipe(parser),在编译期就知道 ,接错了根本编译不过。动态语言里同样的链,错误要到运行时第一行才炸;Rust 在 cargo build 就告诉你。

二、LangGraph 状态图 ------链是「线性」的,工作流是「有分支、有循环」的。lc-langgraph 提供节点 + 边的图:

rust 复制代码
let compiled = GraphBuilder::<AgentState>::new()
    .add_node_fn("greet", |state: &AgentState| {
        Ok(StateUpdate::full(AgentState::new(format!("你好:{}", state.input))))
    })
    .add_node_fn("reply", |state: &AgentState| {
        let mut s = state.clone();
        s.set_output(format!("回复:{}", state.input));
        Ok(StateUpdate::full(s))
    })
    .add_edge(START, "greet")
    .add_edge("greet", "reply")
    .add_edge("reply", END)
    .compile()?;

let result = compiled.invoke(AgentState::new("世界".to_string())).await?;

状态被显式建模(AgentState),节点是纯函数(&state -> Result<StateUpdate>),边决定流向。加条件边、FanOut/FanIn 并行、Reducer 合并、Checkpointer 持久化------Agent 应用的工作流从「if-else 糊在一起」变成「声明式的图」

三、AgentExecutor 循环 ------Agent 的本质是「模型自己决定下一步」。lc-agents 把「翻译」和「执行」拆开:BaseAgent 把模型输出翻译成决定(Action / Finish),AgentExecutor 是那个唯一的真实循环:

rust 复制代码
let tools: Vec<Arc<dyn BaseTool>> = vec![Arc::new(Calculator::new())];
let agent = FunctionCallingAgent::new(llm, tools.clone(), None);
let executor = AgentExecutor::new(Arc::new(agent) as Arc<dyn BaseAgent>, tools)
    .with_max_iterations(3)
    .with_verbose(true);

let result = executor.invoke("What is 25 + 17?".to_string()).await?;

循环不是无限转的------max_iterations 被钳在 [1, 100],工具调用有超时,LLM 调用带指数退避重试,并行动作有信号量限流。这些是「生产加固」,不是可选项:Agent 循环一旦失控,就是无限的 API 账单。

记忆在 lc-memory(缓冲/窗口/摘要/摘要+原始四种),会话在 lc-sessions,检索在 lc-rag(BM25 / 向量 / 混合 RRF 融合 / GraphRAG)。所有检索器实现同一个 RetrieverTrait------换检索策略不换管道代码。

互联层:MCP 工具协议 + A2A 智能体协议

MCP(Model Context Protocol) 让 Agent 调「外部的工具」------一个独立进程暴露的工具。lc-mcp 实现了完整的 client + server,Stdio 和 SSE 两种传输,全部 6 种原语(Resources / Prompts / Completion / Elicitation / Roots / Sampling)。工具经 MCPToolAdapter 实现 BaseTool,MCP 工具和本地工具在 Agent 眼里没有区别:

rust 复制代码
// MCP 工具混进本地工具,同一个 Vec<Arc<dyn BaseTool>>
let tools: Vec<Arc<dyn BaseTool>> = vec![
    Arc::new(Calculator::new()),
    Arc::new(MCPToolAdapter::new(mcp_client.clone(), tool_definition)),
];

A2A(Agent-to-Agent) 解决「Agent 之间怎么派活」。lc-a2a 实现了完整的任务生命周期状态机------9 个状态、14 条合法转移、终态不可复活,加上空串幂等占位挡请求重放、std::sync::Mutex + RAII 守卫防并发抢单、LRU+TTL 管内存。一个 BaseChain 包进 A2AServer 就变成网络上的 Agent:

rust 复制代码
let server = A2AServer::new(Arc::new(chain) as Arc<dyn langchainrust::BaseChain>)
    .with_auth_token("secret-token")
    .with_streaming(64);
server.serve_on(listener).await?;

两者怎么分工 :A2A 管 Agent ↔ Agent,A2A 之下是 MCP 管 Agent → 工具。一个多智能体系统里,顶层 Agent 通过 A2A 给子 Agent 派活,每个 Agent 再通过 MCP 调工具------两个协议各司其职、垂直叠加。

质量层:护栏、评测、可观测

LLM 应用和传统应用最大的区别:输出不可控。质量层是专门跟「不可控」对抗的。

护栏(lc-guardrails) 。输入和输出分开拦,而且类型上就分死------InputGuardrailResult 只有 Pass/Block,OutputGuardrailResult 才有 Modify。「Modify 只能用在输出」这件事被编译器强制执行,不是靠约定:

rust 复制代码
let config = GuardrailsConfig::new()
    .with_input(Arc::new(MaxLengthGuardrail::new(1000)))
    .with_output(Arc::new(SensitiveInfoGuardrail::new()));
let mut guarded = GuardedAgent::new(executor, config);

let result = guarded.invoke("What is 2 + 2?".to_string()).await?;
if guarded.violations().is_empty() {
    println!("→ 未触发任何护栏 ✓");
}

内置护栏里有个值得单独讲的:SensitiveInfoGuardrail 对「普通提及」放行、对「具体模式」拦截------"请联系 user@example.com" 拦(具体邮箱),"你的 password 字段建议改用环境变量" 放行(普通提及)。还可以挂 LLM 裁判对「赋值式提及」(password=hunter2)二次判断真实泄露才拦。护栏不是粗暴的关键词黑名单,是分级、上下文敏感的

评测(lc-evaluation) 。10+ 评测器(ExactMatch / Bleu / EmbeddingSimilarity / LLMAsJudge / Faithfulness......),EvalRunner 把「全部用例 × 全部评测器」跑成一个报告------换模型、换提示词之后质量降没降,一测便知。

可观测(lc-callbacks)SpanGuard 用 RAII 保证追踪 span 一定被关闭------不管链路成功、报错还是 panic,析构函数兜底,「漏关 span」这种可观测性事故从结构上不存在。


高性能从哪来?

标题说是「高性能框架」,那就得回答:性能到底从哪来?不是某一行魔法,是五条设计原则叠出来的:

设计点 性能/可靠性收益 实现手段
类型系统当边界 组合错误、状态错误编译期拦截,不是运行时炸 Runnable 的 pipe 类型;Guardrail 输入/输出类型分离
无 GC、无 GIL 多 worker 真并行;延迟无抖动 所有模型/工具 Send + Sync;tokio async
零成本抽象,该擦除才擦除 泛型组合零开销;异构场景才 dyn Box<dyn BaseChatModel> 仅在需要类型擦除处(如 RouterLLM)
流式一等公民 首 token ~1s;流式结构化输出不断句 stream_chat + PartialJsonParser
资源治理内建 内存不爆、坏节点不拖垮 LLMCache LRU、A2A 存储 LRU+TTL、MCP 退避重连+熔断
生产加固前置 确定性失败路径先消掉 工具超时、LLM 指数退避、max_iterations 钳制、CancellationToken 贯穿

单挑一个最能体现「Rust 味」的点:Runnable 的组合是静态类型,但异构池用类型擦除RouterLLM 里,Box<dyn RoutedModel> 让不同供应商的模型共存一个槽位池,同时 ModelAdapter<M> 在构造时把错误统一成 RouterError------泛型组合零开销、需要多态的地方才擦除,两个都不浪费。同样的设计在动态语言里只能是运行时 dict。

冷启动 也值得一提:一个 agent 服务 cargo build --release 出一个二进制,拷贝即跑,没有解释器启动、没有 import 扫描。需要按秒冷启动一批实例的场景下,这是实打实的部署成本差。


实战:五个能力串成一条链

仓库的 lcel_compose 示例把「提示词 + 记忆 + 模型 + 解析器 + RAG」五个能力 pipe 进一个程序------这就是全家桶的终点形态:

rust 复制代码
// 1. 提示词 + 模型 + 解析器
let qa_chain = prompt.pipe(llm.clone()).pipe(StrOutputParser::new());

// 2. 记忆 + 模型 + 解析器(多轮对话,记忆自动读写)
let chat_chain = RunnableWithMessageHistory::new(llm.clone(), memory)
    .pipe(StrOutputParser::new());

// 3. RAG 链:BM25 本地检索(不依赖向量服务)+ LLM 生成
let rag_chain = RagRunnable::new(Arc::new(pipeline));

// 三个链都是 Runnable,调用方式完全一致
qa_chain.invoke(vars, None).await?;    // 回答问题
chat_chain.invoke("我叫什么名字?".into(), None).await?;  // 记得上一轮
rag_chain.invoke("Rust 有哪些核心特性?".into(), None).await?;  // 检索+生成

想再往上走,把链包进 AgentExecutor 就是会调工具的 Agent;包进 GuardedAgent 就带上护栏;包进 A2AServer 就变成能被别的 Agent 调用的网络服务。从一条链到一个多智能体系统,每一步都只加一层包装,不换地基。


总结

代表 crate 解决的问题
基础层 lc-core lc-schema lc-shared 类型地基:Runnable / Message / Document
能力层 lc-providers lc-embeddings lc-tools 换模型/换向量/写工具,只改构造器
组合层 lc-langgraph lc-agents lc-rag lc-chains 链、状态图、Agent 循环、RAG 编排
互联层 lc-mcp lc-a2a Agent 调外部工具、Agent 之间派活
质量层 lc-guardrails lc-evaluation lc-callbacks 输出不可控的对抗:护栏、评测、可观测

langchainrust 的设计哲学是:LLM 应用的「不可控」,不该靠运行时小心应对,该靠类型系统和架构纪律在编译期就按住一部分、在结构上消灭一部分。无 GC 换来真并行,类型擦除换来组合自由,状态机表换来终态不可复活,RAII 换来资源永不泄漏------每一条都是 Rust 给 LLM 应用工程化的回答。


项目地址:https://github.com/atliliw/langchainrust

相关推荐
欧特克_Glodon1 小时前
OpenCV计算机视觉开发入门与实践<二十二>:图像平滑之线性滤波
c++·人工智能·opencv·计算机视觉
ShiXZ2131 小时前
ChatGPT Plus用户注意:5小时限制今日恢复,额度每5小时自动刷新(且额度又又又重置了)
人工智能·chatgpt
阿童木写作1 小时前
跨马翻译:AI批量图片翻译工具,视频字幕翻译与智能抠图一站式解决
大数据·人工智能·python·音视频
chongzhe1 小时前
AgentConnect:多 Agent 协作的开源版 Claude Tag
人工智能·github·源码
武子康1 小时前
看不见的 Reasoning State,为什么不能拥有看得见的工具权限
人工智能·llm·agent
十三画者1 小时前
【文献分享】abCRISPR:基于深度学习的脱碱基gRNA理性设计框架
人工智能·深度学习·数据挖掘·数据分析·数据可视化
腾视科技-AIoT1 小时前
腾视科技重磅推出TensorAI智能体平台,开启智能助手新体验
大数据·人工智能·科技·ai·ai大模型·ainas·腾视科技
天天代码码天天1 小时前
一个 HTML 文件就能跑 OCR:我把 PP-OCR 装进了浏览器
人工智能
cooldream20091 小时前
Conda 镜像源 404 错误完全解决指南
conda·ai编程