张高兴的大模型开发实战:(二)使用 LangChain 构建本地知识库应用

检索增强生成(Retrieval-Augmented Generation,RAG)是一种优化大型语言模型输出的方法,允许模型在生成回答前,从外部知识库中检索相关信息,而非仅依赖模型内部训练的知识。通过引用外部知识库的信息来生成更准确、实时且可靠的内容,并解决知识过时和幻觉的问题。下面将介绍使用 LangChain 和 Ollama 实现一个本地知识库应用。

基础概念

什么是 LangChain

LangChain 是一个大语言模型(LLM)编程框架,其目的是简化基于大语言模型的应用开发,统一不同大模型的调用方式,开发者无需关心底层 API 差异。

什么是 Ollama

Ollama 是一个开源的本地大语言模型运行框架,其目的是简化在本地设备上部署和运行大型语言模型的流程。它通过容器化管理和标准化接口,让用户能够快速、便捷地使用如 Qwen、Deepseek 等主流模型,无需复杂配置。

环境搭建与配置

安装 Ollama

访问 https://ollama.com/download 下载对应系统的安装文件。Windows 系统执行 .exe 文件安装,Linux 系统执行以下命令安装:

curl -fsSL https://ollama.com/install.sh | sh

访问 https://ollama.com/search 可以查看 Ollama 支持的模型。使用命令行可以下载并运行模型,例如运行 Qwen2.5 7B 模型:

ollama run qwen2.5

安装 LangChain

创建 Python 虚拟环境后,执行命令以下命令:

pip install langchain langchain-ollama langchain-chroma langchain-community chromadb unstructured jq

文档加载

知识库中的文档可以使用 JSON、TXT、PDF、Markdown、网页等格式的数据,只需要选择 LangChain 中合适的解析器即可。例如常见的有:

  • DirectoryLoader:批量加载文件夹中的文档。
  • JSONLoader:加载 JSON 格式的文档。
  • PDFPlumberLoader:处理 PDF 中的内容。
  • WebBaseLoader:爬取网页的内容。

下面使用上一篇博客爬取的新闻数据作为示例,展示如何将文本数据加载到程序中。

# 数据示例
{"date": "2024年2月27日", "author": "党政办", "title": "各部门固定电话一览表", "content": "...", "url": "http://www.xzcit.cn/2018/0710/c2031a53936/page.htm"}
{"date": "2025年3月6日", "author": "保卫处", "title": "我校召开2025年安全工作大会", "content": "...", "url": "https://www.xzcit.cn/2025/0305/c275a56681/page.htm"}
{"date": "2025年3月3日", "author": "党委宣传部", "title": "我校举行2024-2025第二学期开学升国旗仪式暨“开学第一课”", "content": "...", "url": "https://www.xzcit.cn/2025/0303/c275a56667/page.htm"}

加载 JSON 数据

加载 JSON 数据可以使用 JSONLoader 加载,json_lines=True 表示 JSON 文件的格式为 .jsonl

from langchain_ollama.embeddings import OllamaEmbeddings
from langchain.document_loaders import JSONLoader

documents = JSONLoader(file_path='./documents/articles.jsonl', jq_schema='.', text_content=False, json_lines=True).load()

打印变量 documents 查看结果,page_content 为加载文档的内容。可以观察到,JSON 文件中的所有数据、所有字段都被加载到了 documents 变量中。

>>> documents[0]
Document(metadata={'source': 'D:\\GitHub\\langchain-ollama\\documents\\articles.jsonl', 'seq_num': 1}, page_content='{"date": "2024\\u5e742\\u670827\\u65e5", "author": "\\u515a\\u653f\\u529e", "title": "\\u5404\\u90e8\\u95e8\\u56fa\\u5b9a\\u7535\\u8bdd\\u4e00\\u89c8\\u8868", "content": "...", "url": "http://www.xzcit.cn/2018/0710/c2031a53936/page.htm"}')

有些时候并不是所有字段都是我们需要的,可以通过 jq_schema 参数指定需要的字段,下面给出了几种常见的示例:

JSON        -> [{"text": ...}, {"text": ...}, {"text": ...}]
jq_schema   -> ".[].text"

JSON        -> {"key": [{"text": ...}, {"text": ...}, {"text": ...}]}
jq_schema   -> ".key[].text"

JSON        -> ["...", "...", "..."]
jq_schema   -> ".[]"

JSON        -> {"...", "...", "..."}
jq_schema   -> "."

刚刚打印的变量 documents 中,字段 metadata 用于存储文档的元数据。在一些高级检索场景中,除了使用文本内容的向量表示进行相似性搜索,还可以将元数据作为辅助信息进行加权或筛选。下面的例子将 JSON 中的 dateauthorurl 字段添加至 metadata 中。

def metadata_func(record: dict, metadata: dict) -> dict:
    """添加元数据"""
    metadata["title"] = record.get("title")
    metadata["author"] = record.get("author")
    metadata["date"] = record.get("date")
    metadata["source"] = record.get("url")
    return metadata

documents = JSONLoader(file_path='documents/articles.jsonl', jq_schema='.', content_key='content', metadata_func=metadata_func, json_lines=True).load()

再次打印变量 documents 查看结果,可以看到 metadata 中新增了 titleauthordatesource 字段,page_content 中的内容变为了 JSON 中的 content 字段。

>>> documents[0]
Document(metadata={'source': 'http://www.xzcit.cn/2018/0710/c2031a53936/page.htm', 'seq_num': 1, 'title': '各部门固定电话一览表', 'author': '党政办', 'date': '2024年2月27日'}, page_content='...')

加载文件夹中的文档

更常见的情况,数据不会只在一个文档中,如果文档是分散在文件夹中的,可以使用 DirectoryLoader 加载,它会递归加载文件夹中的所有文件。

from langchain.document_loaders import DirectoryLoader, TextLoader

text_loader_kwargs={'encoding': 'utf-8'}    # 设置文本加载器的参数
documents = DirectoryLoader(path='./documents', glob='**/*.txt', show_progress=True, use_multithreading=True, loader_cls=TextLoader, loader_kwargs=text_loader_kwargs).load()

如果加载的单个文档过长,还需要将长文本分割为合理长度的块(Chunk),避免超出 LLM 上下文窗口。RecursiveCharacterTextSplitter 按优先级顺序使用一系列分隔符来分割文本,分割有助于保持文本的逻辑结构,比如保留完整的句子或段落。这里设置了一系列分割符 separators,例如换行、空格、英文和中文标点符号。使用 chunk_size 指定了分割后的文本块的长度为 1000。并设置 chunk_overlap 相邻文本块之间的重叠长度为 20,设置重叠可以使信息不会因为在边界处被切断而丢失。

from langchain.text_splitter import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    separators=[
        "\n\n",
        "\n",
        " ",
        ".",
        ",",
        "\u200B",  # Zero-width space
        "\uff0c",  # Fullwidth comma
        "\u3001",  # Ideographic comma
        "\uff0e",  # Fullwidth full stop
        "\u3002",  # Ideographic full stop
        "",
    ], chunk_size=1000, chunk_overlap=20, add_start_index=True)
split_docs = text_splitter.split_documents(documents)

文本向量化

大模型无法直接理解知识库当中的文本,我们需要将人类可读的文本数据转换成机器能够理解和处理的数值形式,这个过程叫做文本向量化。如果将大模型通过词条查找内容的过程比作查字典,那么文本向量化就是在编写字典。这里使用向量化的一种方式—— Embedding(嵌入),能够使向量捕捉词语之间的语义关系、语法相似性等信息。OllamaEmbeddings 是一个大语言模型的 Embedding 工具,将文本数据转换为向量表示,以便于后续的检索和生成。在使用之前还需要下载 Embedding 模型,例如在命令行中执行 ollama pull nomic-embed-text。向量化后还需要使用向量数据库进行存储,例如 FAISSChroma 等。

from langchain_ollama.embeddings import OllamaEmbeddings
from langchain_chroma.vectorstores import Chroma

embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma.from_documents(documents, embeddings, persist_directory="./embeddings")

等待这一过程完成后,试着检索文档,返回与查询语句最相似的文档。

results = db.similarity_search("招聘公告")

for result in results:
    print("Title:", result.metadata["title"])
    print("Author:", result.metadata["author"])
    print("Date:", result.metadata["date"])
    print("Content:", result.page_content)
    print("------\n")

也可以使用向量进行检索,k 指定了返回结果的个数,filter 设置了元数据字段的搜索条件。

query_vector = embeddings.embed_query("招聘公告")
results = db.similarity_search_by_vector(query_vector, k=1, filter={"author": "人事处"})

实现问答应用

首先引入相关的包并加载文档与模型。

from langchain_ollama.llms import OllamaLLM
from langchain_ollama.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

model = OllamaLLM(model="qwen2.5:7b")
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma(embedding_function=embeddings, persist_directory='./embeddings')
retriever=db.as_retriever(search_kwargs={"k": 3})

接着使用 ChatPromptTemplate 定义与用户交互时使用的提示模板。这个模板包含了两个占位符 {context}{question},分别代表从知识库中检索到的相关内容和用户的提问。

template = """你是徐州工业职业技术学院的专业助手,能够根据知识库中的新闻稿回答问题:
{context}
问题:{question}
回答应简洁且准确,避免编造信息。
"""
prompt = ChatPromptTemplate.from_template(template)

然后使用 LangChain 中的一个关键概念——链式调用,构建了一个数据处理管道:首先通过检索器获取相关上下文,然后将其与问题一起格式化成提示,接着发送给语言模型以生成回答,最后通过 StrOutputParser 将输出解析为字符串。

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

最后使用 invoke() 方法执行管道,传入问题作为参数,即可得到回答。

response = chain.invoke("学校最近一次招聘是在什么时候")
print(f"AI: {response}")

完整的程序如下:

from langchain_ollama.llms import OllamaLLM
from langchain_ollama.embeddings import OllamaEmbeddings
from langchain_chroma import Chroma
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

model = OllamaLLM(model="qwen2.5:0.5b")
embeddings = OllamaEmbeddings(model="nomic-embed-text")
db = Chroma(embedding_function=embeddings, persist_directory='./embeddings')
retriever=db.as_retriever(search_kwargs={"k": 3})

template = """你是徐州工业职业技术学院的专业助手,能够根据知识库中的新闻稿回答问题:
{context}
问题:{question}
回答应简洁且准确,避免编造信息。
"""
prompt = ChatPromptTemplate.from_template(template)

chain = (
    {"context": retriever, "question": RunnablePassthrough()}
    | prompt
    | model
    | StrOutputParser()
)

def chat_with_system(question):
    response = chain.invoke(question)
    print(f"AI: {response}")
    return response

# 示例对话
if __name__ == "__main__":
    while True:
        user_input = input("You: ")
        if user_input.lower() in ["exit", "quit"]:
            break
        response = chat_with_system(user_input)
<think>我们正在使用虾哥开源的小智项目(xiaozhi-esp32)通过MCP协议控制LED灯。MCP(Micro-Controller Protocol)是该项目中用于设备间通信的协议。以下是具体步骤:###一、硬件连接-LED正极 →ESP32的GPIO引脚(例如GPIO2,串联限流电阻) -LED负极 →GND###、代码修改####1.注册LED设备在`device_manager.cpp`中注册LED设备: ```cpp#include "devices/led_device.h"//包含LED设备头文件void DeviceManager::InitializeDevices(){// ...其他设备初始化代码AddDevice(std::make_shared<LedDevice>());//注册LED设备}``` ####2.创建LED设备类- **头文件**`led_device.h`:```cpp#pragmaonce#include "thing.h"class LedDevice :public Thing{ public:LedDevice();void TurnOn();voidTurnOff();//通过MCP协议控制需要实现HandleMethod方法boolHandleMethod(const std::string& method, constJsonObject& params, JsonObject&result)override;private:boolstatus_;constgpio_num_tgpio_ =GPIO_NUM_2;//使用GPIO2}; ```- **实现文件**`led_device.cpp`:```cpp#include"led_device.h" #include<driver/gpio.h> LedDevice::LedDevice() :Thing("LED","MCP控制的LED灯") {gpio_reset_pin(gpio_);gpio_set_direction(gpio_,GPIO_MODE_OUTPUT);gpio_set_level(gpio_,0);//初始关闭status_ =false;//注册属性(用于MCP协议状态报告)properties_.AddBooleanProperty("status","当前状态",[this](){return status_; });//注册方法(MCP调用)methods_.AddMethod("TurnOn","开灯",ParameterList(), [this](constParameterList&){ TurnOn();});methods_.AddMethod("TurnOff","关灯",ParameterList(), [this](const ParameterList&) {TurnOff(); });} voidLedDevice::TurnOn(){status_= true;gpio_set_level(gpio_,1);//高电平点亮} voidLedDevice::TurnOff(){status_= false;gpio_set_level(gpio_,0);//低电平熄灭}//处理MCP调用boolLedDevice::HandleMethod(conststd::string&method,const JsonObject&params,JsonObject& result) {if(method== "TurnOn") {TurnOn();result["status"] ="success";return true;} elseif (method =="TurnOff"){TurnOff();result["status"]= "success";returntrue;}returnfalse;//方法未处理}``` ###三、通过MCP协议控制LED小智项目内置了MCP服务器,其他设备(如手机APP、网页)可通过MCP协议发送控制命令。协议格式示例: ```json{"device": "LED",//设备名称"method": "TurnOn",//方法名"params":{}//参数(本例无参数) }```###四、测试MCP控制1. **启动MCP服务器**:小智项目启动时会自动监听MCP端口(默认`6789`)。 2. **发送控制命令**(使用`netcat`测试):```bashecho '{"device":"LED","method":"TurnOn","params":{}}' |nc192.168.1.1006789```响应示例:```json{"status":"success"}``` 3. **观察LED状态**:发送命令后LED应点亮。###五、结合BotSharp智能体框架(可选) BotSharp可作为上层应用,通过MCP协议控制小智设备[^2]。在BotSharp中创建智能体时,调用MCP接口:```csharp// BotSharp智能体示例代码publicasyncTask<string> ControlLed(string deviceIp,stringaction) {using(varclient =new HttpClient()){var mcpCommand= new{device= "LED",method= action== "on"? "TurnOn" :"TurnOff",params= new{ }};varresponse= awaitclient.PostAsJsonAsync($"http://{deviceIp}:6789", mcpCommand);return awaitresponse.Content.ReadAsStringAsync();} }```>提示:实际开发中需处理网络异常和超时。§§相关问题§§1.MCP协议如何实现设备发现? 2.如何在小智项目中加密MCP通信? 3.如何通过MQTT转发MCP命令实现远程控制? 4. BotSharp智能体如何解析自然语言生成MCP命令? [^1]:创客社区地址BotSharp文档桌面桌面机器人仓库地址.NETIoT库源码地址.NET IoT官方地址.NET IoT社区人员高兴推荐什么是lottie动画?参考的语音对话项目conversational-speaker小智C#客户端C#MCPSDK树莓派学习之旅(2)—使用USB声卡进行录音BotSharp官方GitHubEdgeVoiceAI聊天机器人[^2]:结合BotSharp智能体框架开发语音交互。BotSharp是一个开源的多智能体应用开发框架,从简单的聊天机器人,再到多智能体协作,以及复杂的任务如【TextTo Sql】框架都提供了开箱即用的使用方法,可以快速的将大模型的能力接入到现有的业务系统中,并且内置知识库和会话管理功能等。大语言模型的函数调用(这个是理解BotSharp框架的核心知识点)。</think>### 如何使用虾哥开源小智项目通过MCP协议控制LED灯 以下是基于小智项目通过MCP协议实现LED控制的完整步骤,结合硬件连接、代码修改和协议配置: #### 一、硬件准备 1. **ESP32开发板**:使用支持MCP协议的ESP32开发板 2. **LED连接**: - LED正极 → GPIO2(串联220Ω限流电阻) - LED负极 → GND 3. **网络连接**:确保ESP32接入局域网 #### 、代码修改(小智项目端) ##### 1. 创建LED设备类 在`devices`目录下创建`led_device.h`: ```cpp #pragma once #include "thing.h" #include <driver/gpio.h> class LedDevice : public Thing { public: LedDevice(); void TurnOn(); void TurnOff(); bool HandleMCPCommand(const std::string& method, const JsonObject& params); // MCP协议处理 private: bool status_ = false; const gpio_num_t gpio_ = GPIO_NUM_2; // 使用GPIO2 }; ``` ##### 2. 实现LED控制逻辑(`led_device.cpp`) ```cpp #include "led_device.h" LedDevice::LedDevice() : Thing("LED", "MCP控制的LED灯") { gpio_reset_pin(gpio_); gpio_set_direction(gpio_, GPIO_MODE_OUTPUT); gpio_set_level(gpio_, 0); // 初始关闭 // 注册MCP可调用方法 methods_.AddMethod("TurnOn", "开灯", ParameterList()); methods_.AddMethod("TurnOff", "关灯", ParameterList()); } void LedDevice::TurnOn() { status_ = true; gpio_set_level(gpio_, 1); // 高电平点亮 } void LedDevice::TurnOff() { status_ = false; gpio_set_level(gpio_, 0); // 低电平熄灭 } // 处理MCP协议命令 bool LedDevice::HandleMCPCommand(const std::string& method, const JsonObject& params) { if (method == "TurnOn") { TurnOn(); return true; } else if (method == "TurnOff") { TurnOff(); return true; } return false; } ``` ##### 3. 注册设备并启用MCP服务 在`main.cpp`中: ```cpp #include "devices/led_device.h" #include "network/mcp_server.h" // MCP协议服务 void setup() { // ...其他初始化 DeviceManager::AddDevice(std::make_shared<LedDevice>()); // 启动MCP服务器(默认端口6789) MCPServer::Start(6789); } ``` #### 三、MCP协议控制端实现(C#示例) 使用C# MCP SDK发送控制命令[^1]: ```csharp using MCP.Client; class Program { static void Main() { // 连接到小智设备 var device = new McpDevice("192.168.1.100", 6789); // ESP32的IP // 发送开灯命令 var response = device.InvokeMethod("LED", "TurnOn"); if (response.Status == "success") { Console.WriteLine("开灯成功"); } else { Console.WriteLine($"控制失败: {response.Error}"); } } } ``` #### 四、MCP协议命令格式 通过TCP发送的原始MCP协议格式: ```json { "device": "LED", // 设备名称 "method": "TurnOn", // 调用的方法 "params": {}, // 参数(本例不需要) "request_id": 123 // 请求ID(可选) } ``` 成功响应示例: ```json { "status": "success", "device": "LED", "response": {} } ``` #### 五、测试流程 1. 烧录修改后的小智固件到ESP32 2. 启动C#控制程序(或使用网络调试工具发送MCP命令) 3. 观察LED状态变化 4. 调试命令: ```bash echo '{"device":"LED","method":"TurnOn"}' | nc 192.168.1.100 6789 ``` > 提示:BotSharp框架可作为上层应用生成MCP命令,实现自然语言控制LED[^2]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值