构建RAG智能体(1):现代RAG开发框架之LangChain LCEL

RAG通过结合外部知识检索和大型语言模型的生成能力,能够提供更准确、更具时效性的回答,已经成为企业级AI应用的标准架构。然而,在实际开发RAG系统时,开发者往往面临着组件编排复杂、数据流管理困难、服务部署繁琐等挑战。

如何优雅地将检索器、生成器、提示模板等组件串联起来?如何实现流式响应以提升用户体验?如何将复杂的AI逻辑封装成可复用的服务?

LangChain Expression Language(LCEL)为这些问题提供了现代化的解决方案。

1 LangChain与LCEL概述

LangChain作为目前最流行的LLM编排库,经历了从传统Chain模式到现代LCEL的重要演进。传统的Chain系统虽然功能完整,但在复杂场景下往往显得繁琐和难以维护。LCEL的出现彻底改变了这一状况,它引入了Runnable这一核心概念,将函数包装成标准化的可执行对象。

在这里插入图片描述

LCEL的核心优势在于其管道操作符|的设计。通过简单的管道语法,开发者可以将多个功能模块串联起来,形成复杂的处理流程。这种设计不仅代码简洁,而且具有良好的可读性和可维护性。

Runnable核心概念

Runnable是LCEL的基础构建块,它将任意函数包装成具有统一接口的可执行对象。所有Runnable都提供相同的核心方法:invoke(单次调用)、stream(流式调用)、batch(批量调用)等。这种统一的接口设计使得不同组件之间可以无缝集成。

from langchain.schema.runnable import RunnableLambda, RunnablePassthrough
from functools import partial

## 一个非常简单的例子:接收输入并原样返回
identity = RunnableLambda(lambda x: x)  ## 或者可以用 RunnablePassthrough 实现同样效果

## 给定任意一个函数,你都可以将其包装成一个 Runnable
def print_and_return(x, preface=""):
    print(f"{preface}{x}")
    return x

rprint0 = RunnableLambda(print_and_return)  ## 未设置前缀,直接打印输入

## 你可以使用 functools.partial 预设部分参数值
rprint1 = RunnableLambda(partial(print_and_return, preface="1: "))

## 也可以封装为你自己的自定义 Runnable 生成器
def RPrint(preface=""):
    return RunnableLambda(partial(print_and_return, preface=preface))

## 将两个 Runnable 串联起来
chain1 = identity | rprint0
chain1.invoke("Hello World!")  ## 输出 "Hello World!" 并返回

print()

## 继续将多个 Runnable 串联起来
output = (
    chain1           ## 打印 "Welcome Home!",并将其传递下去
    | rprint1        ## 打印 "1: Welcome Home!",继续传递
    | RPrint("2: ")  ## 打印 "2: Welcome Home!",继续传递
).invoke("Welcome Home!")

## 最终输出结果仍然是原始输入值 "Welcome Home!"
print("\nOutput:", output)

输出:

Hello World!

Welcome Home!
1: Welcome Home!
2: Welcome Home!

Output: Welcome Home!

2 字典管道与聊天模型集成

在LCEL中,字典被广泛用作数据容器,这种设计有着深刻的考虑。字典允许我们通过键名来管理变量,这对于复杂的数据流处理至关重要。同时,LangChain的提示模板系统天然支持字典输入,这使得整个处理流程更加自然。

使用字典的另一个重要原因是它与LangChain的隐式Runnable语法完美契合。当我们使用字典作为Runnable时,系统会自动将每个键值对转换为并行执行的任务,大大简化了复杂逻辑的实现。

2.1 简单LLM链的构建

构建一个基础的LLM链通常包含三个核心组件:提示模板、语言模型和输出解析器。这种模式在RAG系统中尤为常见,是构建更复杂功能的基础。

from langchain_nvidia_ai_endpoints import ChatNVIDIA
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

# 创建聊天链
chat_llm = ChatNVIDIA(model="meta/llama3-8b-instruct")

prompt = ChatPromptTemplate.from_messages([
    ("system", "只用押韵的方式回答"),
    ("user", "{input}")
])

rhyme_chain = prompt | chat_llm | StrOutputParser()
result = rhyme_chain.invoke({"input": "告诉我关于鸟类的知识"})

输出:

Birds are quite a delightful find,
With feathers and wings, they soar and entwine.
In trees, they alight, with tails so bright,
And sing their songs, with morning light.

Some have beaks that curve, some have beaks that straight,
Their chirps and chatter, fill the air and create
A symphony sweet, of melodic sound,
As birds take flight, their magic's all around.

From robins to sparrows, to hawks on high,
Each species unique, yet all touch the sky.
With colors bright, and forms so grand,
Birds are a wonder, in this world so bland.

你可以将检索到的上下文填入提示模板,再通过LLM生成回答,最后用解析器提取文本结果,实现从检索到问答的高效对接。

2.2 零样本分类的实现

有时候,在向用户输出回答之前,你可能希望模型先进行一些快速的内部推理。在这种任务中,模型需要具备很强的指令遵循能力(instruction-following bias)。

下面是一个零样本分类(zero-shot classification)的示例流程,它尝试将一句话分类为给定的选项之一。

from langchain_nvidia_ai_endpoints import ChatNVIDIA
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

## 模型列表可参考:https://build.nvidia.com
instruct_llm = ChatNVIDIA(model="mistralai/mistral-7b-instruct-v0.2")

# 系统提示词:引导模型根据句子内容选择最可能的分类(仅返回一个词,无解释)
sys_msg = (
    "Choose the most likely topic classification given the sentence as context."
    " Only one word, no explanation.\n[Options : {options}]"
)

## 一种带有强格式约定的提示词写法(单轮示例,One-shot示例)
zsc_prompt = ChatPromptTemplate.from_messages([
    ("system", sys_msg),
    ("user", "[[The sea is awesome]]"),
    ("assistant", "boat"),
    ("user", "[[{input}]]"),
])

## 另一种提示写法,模拟 HuggingFace 格式(INST/response 样式),效果类似
zsc_prompt = ChatPromptTemplate.from_template(
    f"{sys_msg}\n\n"
    "[[The sea is awesome]][/INST]boat</s><s>[INST]"
    "[[{input}]]"
)

# 构建完整的分类链:提示词 → 模型 → 提取纯文本输出
zsc_chain = zsc_prompt | instruct_llm | StrOutputParser()

# 调用函数:传入文本 input 和分类选项,返回模型给出的第一个词(预测标签)
def zsc_call(input, options=["car", "boat", "airplane", "bike"]):
    return zsc_chain.invoke({"input" : input, "options" : options}).split()[0]

# 测试示例句子分类
print("-" * 80)
print(zsc_call("Should I take the next exit, or keep going to the next one?"))

print("-" * 80)
print(zsc_call("I get seasick, so I think I'll pass on the trip"))

print("-" * 80)
print(zsc_call("I'm scared of heights, so flying probably isn't for me"))

输出:

--------------------------------------------------------------------------------
car
--------------------------------------------------------------------------------
boat
--------------------------------------------------------------------------------
airplane

这个零样本分类链的执行流程如下:

  • 接收一个字典输入,包含两个必须的键:input(待分类文本)和options(分类选项列表)
  • 将其传入一个分类提示词模板,生成传给语言模型的输入,问大模型这个文本对应哪个分类最好
  • 最后再解析LLM的输出,看这个文本对应哪个分类

选择合适的模型对于内部推理任务至关重要。理想的模型应该具备两个特点:输出格式可预测且响应速度快。因为内部推理通常是用户不可见的处理步骤,过慢的响应会影响整体用户体验。

2.3 多组件链的协调

前面的例子展示了如何通过提示模板 → LLM的链条将一个字典转化为字符串,这种方式为我们使用字典作为容器提供了结构上的动机。那么,是否也能轻松地将字符串输出重新转化为字典呢?

最简单的方法是使用LCEL的隐式Runnable语法,你可以将一个包含函数(包括子链)的字典当作一个整体的 Runnable来使用。执行时,它会将输入依次传入每个函数,并将结果按键名存入输出字典中。

from langchain.schema.runnable import RunnableLambda, RunnablePassthrough
from functools import partial

## 字典封装的辅助方法
def make_dictionary(v, key):
    if isinstance(v, dict):
        return v
    return {key : v}

def RInput(key='input'):
    '''将一个值(如字符串)包装成包含特定键的字典'''
    return RunnableLambda(partial(make_dictionary, key=key))

def ROutput(key='output'):
    '''将任意值包装成以某键为名的字典格式'''
    return RunnableLambda(partial(make_dictionary, key=key))

def RPrint(preface=""):
    '''自定义打印函数包装成 Runnable,可插入链中打印中间过程'''
    return RunnableLambda(partial(print_and_return, preface=preface))

## 常用 LCEL 工具:从字典中提取值
from operator import itemgetter

up_and_down = (
    RPrint("A: ")         # 打印原始输入
    | RInput()            # 确保输入是 {'input': ...} 的格式
    | RPrint("B: ")       # 打印变换后的字典格式
    | itemgetter("input") # 提取 'input' 键对应的值
    | RPrint("C: ")       # 打印提取出来的值
    # 字符串 → 多路并发处理 → 输出字典
    | {
        'word1' : (lambda x : x.split()[0]),  # 提取第一个单词
        'word2' : (lambda x : x.split()[1]),  # 提取第二个单词
        'words' : (lambda x: x),              # 保持原样(等价于 RunnablePassthrough)
    }
    | RPrint("D: ")       # 打印刚刚生成的字典
    | itemgetter("word1") # 从结果中取出 word1
    | RPrint("E: ")       # 打印 word1
    | RunnableLambda(lambda x: x.upper()) # 转换为大写
    | RPrint("F: ")       # 打印最终处理结果
    | ROutput()           # 包装为 {'output': ...} 格式返回
)


输入一个字典,开始执行整个链条:

up_and_down.invoke({"input" : "Hello World"})

输出:
A: {'input': 'Hello World'}
B: {'input': 'Hello World'}
C: Hello World
D: {'word1': 'Hello', 'word2': 'World', 'words': 'Hello World'}
E: Hello
F: HELLO

{'output': 'HELLO'}

输入一个字符串:

  • | RInput():把任何非字典输入转成{'input': ...}
up_and_down.invoke("Hello World")

输出:
A: Hello World
B: {'input': 'Hello World'}
C: Hello World
D: {'word1': 'Hello', 'word2': 'World', 'words': 'Hello World'}
E: Hello
F: HELLO

{'output': 'HELLO'}

这段代码通过LangChain的表达式语言(LCEL)构建了一个完整的数据处理链条,演示了如何将任意输入(如字符串Hello World)转换为结构化输出。

执行流程如下:首先输入通过RPrint("A: ")打印原始内容,然后经过RInput()模块将输入包装成标准字典 {"input": ...};接着用itemgetter("input")取出字典中的值,再通过一个包含多个函数的隐式runnable字典,分别提取出第一个词、第二个词和完整句子,并打包为新的字典;随后使用itemgetter("word1")提取第一个词并转为大写。

整个过程利用LCEL的模块化和链式组合能力,实现了从原始输入到结构化字典输出的完整流程,目的是展示如何灵活地进行输入封装、字段提取、并行处理和输出格式转换。这在构建更复杂的RAG或数据处理流水线中非常有用。

3 例子:聊天机器人

以下是一个诗歌生成的示例,它展示了如何将两个不同的任务组合在一个Agent中完成。

它的核心特性如下:

  • 第一次回复:根据用户输入的主题生成一首原创押韵诗;
  • 后续回复:保留原始诗歌的格式和结构,但更换主题,改写为关于新主题的诗。
from langchain_nvidia_ai_endpoints import ChatNVIDIA
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from copy import deepcopy

# 初始化语言模型(可按需更换模型 ID)
instruct_llm = ChatNVIDIA(model="mistralai/mixtral-8x22b-instruct-v0.1")

# 第一个提示模板:根据用户输入生成押韵诗
prompt1 = ChatPromptTemplate.from_messages([("user", (
    "INSTRUCTION: Only respond in rhymes"
    "\n\nPROMPT: {input}"
))])

# 第二个提示模板:修改原始诗歌的主题(保持结构与韵律)
prompt2 = ChatPromptTemplate.from_messages([("user", (
    "INSTRUCTION: Only responding in rhyme, change the topic of the input poem to be about {topic}!"
    " Make it happy! Try to keep the same sentence structure, but make sure it's easy to recite!"
    " Try not to rhyme a word with itself."
    "\n\nOriginal Poem: {input}"
    "\n\nNew Topic: {topic}"
))])

# 定义两条主链:一个用于初始生成,一个用于主题改写
chain1 = prompt1 | instruct_llm | StrOutputParser()
chain2 = prompt2 | instruct_llm | StrOutputParser()

# 核心生成函数:根据历史记录判断是首次创作还是改写主题
def rhyme_chat2_stream(message, history, return_buffer=True):
    '''生成器函数,每次调用返回一个新响应内容'''
    first_poem = None
    for entry in history:
        if entry[0] and entry[1]:
            # 从聊天记录中提取第一首生成的诗
            first_poem = entry[1]
            break

    if first_poem is None:
        # 情况 1:首次创作,没有现有诗歌

        buffer = "Oh! I can make a wonderful poem about that! Let me think!\n\n"
        yield buffer

        # 使用 chain1 流式生成诗歌
        inst_out = ""
        chat_gen = chain1.stream({"input": message})
        for token in chat_gen:
            inst_out += token
            buffer += token
            yield buffer if return_buffer else token

        # 提示用户提供一个新主题用于改写
        passage = "\n\nNow let me rewrite it with a different focus! What should the new focus be?"
        buffer += passage
        yield buffer if return_buffer else passage

    else:
        # 情况 2:已有一首诗,用新主题改写它

        buffer = f"Sure! Here you go!\n\n"
        yield buffer

        # 使用 chain2 进行主题改写(流式生成)
        chat_gen = chain2.stream({"input": first_poem, "topic": message})
        for token in chat_gen:
            buffer += token
            yield buffer if return_buffer else token

        # 提示用户继续输入新主题
        passage = "\n\nThis is fun! Give me another topic!"
        buffer += passage
        yield buffer if return_buffer else passage

# 简化版 Gradio 聊天事件循环模拟(基于命令行输入)
def queue_fake_streaming_gradio(chat_stream, history = [], max_questions=3):

    # 初始化聊天记录,打印过往消息
    for human_msg, agent_msg in history:
        if human_msg: print("\n[ Human ]:", human_msg)
        if agent_msg: print("\n[ Agent ]:", agent_msg)

    # 模拟聊天循环,最多进行 max_questions 轮对话
    for _ in range(max_questions):
        message = input("\n[ Human ]: ")
        print("\n[ Agent ]: ")
        history_entry = [message, ""]
        for token in chat_stream(message, history, return_buffer=False):
            print(token, end='')
            history_entry[1] += token
        history += [history_entry]
        print("\n")

# 聊天历史格式:[[用户输入,模型响应],...]
history = [[None, "Let me help you make a poem! What would you like for me to write?"]]

# 启动模拟聊天界面(命令行交互版)
queue_fake_streaming_gradio(
    chat_stream = rhyme_chat2_stream,
    history = history
)

输出如下:

[ Agent ]: Let me help you make a poem! What would you like for me to write?


[ Human ]:  Birds


[ Agent ]: 
Oh! I can make a wonderful poem about that! Let me think!

In the sky, so high and free,
Chirping tunes for all to see,
Fluttering wings in harmony,
Oh, the beauty of birds' symphony.

Robins red on morning dew,
Sparrows grey that bid adieu,
Eagles soaring in hues of blue,
Birds of every kind, their colors true.

In nests they rest, in flocks they fly,
With nature's rhythm, they comply,
Melodies in the sunrise sky,
Birds, oh birds, your charm won't die.

Now let me rewrite it with a different focus! What should the new focus be?



[ Human ]:  Sky


[ Agent ]: 
Sure! Here you go!

Oh! I can make a wonderful poem about that sky! Allow me to think!

In the sky, so high and bright,
Sparkling stars in deepest night,
Cotton clouds, light and white,
Oh, the magic of celestial sight!

Sun so gold as it starts the day,
Moonbeams silver in night's array,
Comets blaze in a dazzling play,
Colors dancing where planets sway.

In space they roam, in skies they soar,
Celestial bodies, we explore,
Galaxies spin, and more and more,
Sky's enchantment that we adore.

Now let me rewrite it with yet a different focus! What should the new focus be?

This is fun! Give me another topic!



[ Human ]:  galaxies


[ Agent ]: 
Sure! Here you go!

In the cosmos, vast and wide,
Worlds abundant, side by side,
Stardust all around, a smooth glide,
Oh, the splendor of galaxies, my pride!

Spirals bright in colors of white,
Elliptical forms in the night,
Pulsars spinning with all their might,
Galaxies twirling, oh, what a sight!

In clusters, they dance with such art,
Galaxies billions, set far apart,
In our hearts, they play a sweet part,
Galaxies, oh galaxies, you're a work of art.

With planets and moons within their bounds,
In constellations, galaxies resound,
Every single one is heavenly crowned,
Galaxies, oh galaxies, no words profound!

This is fun! Give me another topic!

这个押韵聊天机器人通过检查聊天历史history来判断是否已经生成过一首诗。如果是第一次输入,则使用chain1根据用户输入生成原创押韵诗;如果历史中已有诗歌(即模型前一次的输出),则将那首诗作为input,用户当前输入作为topic,交给chain2来生成一首同样格式但主题改写的新诗。

这个结构适合构建具备上下文意识的创作型聊天机器人,它展示了如何用链式结构(prompt + LLM + parser)结合简易历史追踪。

4 总结

LCEL的管道语法不仅简化了代码编写,更重要的是它体现了一种声明式的编程思维。这种思维方式在处理复杂的AI工作流时具有独特优势,能够让开发者更专注于业务逻辑而非技术细节。

### 使用LangChain构建RAG构建检索增强生成(RAG)应用时,LangChain提供了强大的工具集来简化这一过程。具体来说,`langchain.utils`模块中的实用函数可以辅助处理数据准备和其他预处理工作[^1]。 对于核心的RAG实现,LangChain引入了可运行组件(Runnable)以及表达式语言(LCEL),这使得定义复杂的流水线变得简单直观[^2]。下面是一个简单的例子展示如何利用这些特性创建一个基本的RAG系统: ```python from langchain import LangChain, DocumentLoader, EmbeddingModel, VectorStore, Retriever, PromptTemplate, LLMChain # 初始化文档加载器用于读取并分割输入文本文件 document_loader = DocumentLoader() # 加载嵌入模型以转换文本片段成向量表示形式 embedding_model = EmbeddingModel() # 创建矢量存储实例保存经过编码后的文档片断 vector_store = VectorStore(embedding_function=embedding_model.embed) # 构建检索器对象以便后续查询最相似的内容项 retriever = Retriever(vector_store=vector_store) # 定义提示模板指导大语言模型(LLM)基于检索到的信息生成回复 prompt_template = PromptTemplate( input_variables=["context", "question"], template="Given this context:\n{context}\nAnswer the following question: {question}" ) # 组合上述各部分形成完整的LLM链路结构 rag_chain = LLMChain(llm=LangChain(), retriever=retriever, prompt=prompt_template) ``` 此代码段展示了如何集成不同类型的组件——从原始资料加载直至最终响应合成——从而搭建起一套有效的RAG解决方案。值得注意的是,在实际部署过程中可能还需要考虑更多细节配置选项和服务优化措施。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tilblackout

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值