LangChain+本地大模型实现对话问答开发入门案例

        LangChain是一个AI应用快速开发的代码层面框架,类似我们用Jedis组件去访问Redis服务一样。大语言模型LLM就相当于这里的Redis服务一样,独立部署的第三方中间件。所以开发时需要先部署一个大语言模型,可以是直接使用第三方厂商已经部署好的使用API+秘钥方式访问。这里我们使用本地部署大模型方式进行模拟开发。同时在Python和java开发环境下进行验证示例。

大语言模型本地部署

        大语言模型LLM本地也是可以部署的,只是相当于使用了一个阉割版(参数很少)。使用meta公司的Ollama工具可以非常方便的将大语言模型下载到本地并部署服务。

下载Ollama工具

        去Ollama官网:Ollama 下载最新版本的Windows安装包,安装到本地就行。如果下载很慢,可以去其他国内网站下一个。安装后默认就启动了,会在桌面右下角有个服务图标,下载或导入的模型自动就处于可用状态了。

模型选取

        去Ollama官网的“Models”菜单里找个大语言模型,这里有很多大模型,主流开源的都在里面,还有各种网友自己经过微调训练的模型。

        为了能在本地顺利部署一个可以运行的模型,模型的参数选取不宜过大,7B以下为宜,否则可能启动不起来。1B就是10亿参数量的模型。参数量越大代表模型回答问题表现越好,想要获得较好的表现可能需要部署一个14B以上的,这个需要你电脑配置较高才行,起码内存、CPU足够,再大的模型可能需要专业的CPU显卡才能玩。

模型下载和管理

        通过本地CMD命令窗口使用ollama命令就可以完成大模型下载并启动。将上图中右边的CMD命令复制粘贴到本地命令窗口即可(ollama run qwen2.5:0.5b)。会自动下载模板模型,下载过程还有进度条展示,完成后会默认启动模型进入交互式会话模式。

        这种在线现在模型,国内可能非常慢,体积大的最好去其他国内模型网站上去手动下载,然后部署到本地。魔塔社区:魔搭社区 提供了非常丰富的各种模型,下载速度也很好。需要找那种GGUF格式的模型文件下载。比如:DeepSeek-R1-Distill-Qwen-14B-GGUF · 模型库 下载单个.gguf文件即可

        模型下载后,接下来就是要将其导入到ollama服务中去了,在模型所在文件夹中新建一个Modelfile文本文件,并写入信息:FROM D:\LLM\DeepSeek-R1-Distill-Qwen-14B-Q2_K.gguf 填写自己的模型文件路径保存即可。截图可能和上面描述的文件名称不一致,请使用自己实际的模型文件即可。

        然后再当前文件目录下打开CMD命令窗口,输入命令:ollama create deepseek_r1_7b -f ./Modelfile 执行即可将此大模型导入到ollama服务中去。

        使用命令:ollama list 可以查看本地ollama服务管理的所有可用模型,包括上面成功导入的模型

        ollama管理的模型文件在:C:\Users\kingdee.ollama\models\blobs 本地用户空间中,包括在线下载和上面导入的外部模型。外部模型导入后会在这里复制一份新的模型文件,所以导入后可以删除外部下载的模型文件。模型文件名称没法区分,可以通过大小进行识别。

        如果你不想要某个模型了,可以通过命令:ollama rm qwen2.5-3bnsfw:latest 删除指定名称的模型。

模型交互

        模型启动或切换可以通过命令:ollama run deepseek_r1_15b:latest 执行即可,然后窗口会出现一个用户可以输入的模型。直接输入你的问题就可以跟大模型进行对话了。这里只是一个简单的场景,我们最终还是会通过代码进行交互。

        如果模型参数较大,连接模型进行会话时可能会报错如下图。错误信息说明当前机器内存不够。试着关闭桌面更多耗内存的应用重新连接,是有可能成功的。

        大模型在运行时是非常消耗内存和CPU的

使用LangChain框架开发问答系统

安装Python环境的langChain开发库

        首先确保本地已经安装好了Python开发环境,然后安装langChain库即可。打开CMD命令窗口输入:pip install langchain 执行后pip工具就会自动将组件下载到你本地Python库里。如果需要指定版本则输入:pip install langchain==0.3.0 当前我的测试机安装的是这一个版本,因为在使用过程中遇到最新版本API不熟悉问题,导致运行demo时各种报错。

        安装完后,可以在你开发工具PyCharm中的外部库“site-packages”中查看是否安装好,也可以在Python安装目录下查看,如下图所示。

安装java环境的langChain开发库

        langChain在java开发环境下对应的组件是langChain4j,为方便java环境开发专门做的移植。可以在Maven仓库:https://mvnrepository.com/artifact/dev.langchain4j 中查看所有版本及其他子模块的依赖配置。需要注意的是最新版本可能需要你本地JDK版本在8以上,但是如果公司只能依赖低版本的JDK的话,只能选用0.35.0版本了,不然启动就会报版本错误。

<!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j</artifactId>
    <version>0.35.0</version>
    <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/dev.langchain4j/langchain4j-ollama -->
<dependency>
    <groupId>dev.langchain4j</groupId>
    <artifactId>langchain4j-ollama</artifactId>
    <version>0.35.0</version>
</dependency>

开发一个能和大模型交互问答系统

一个简单的和大模型交互的例子

如下所示,通过langChain这个组件开发框架可以非常方便的跟本地大模型进行对话。

        Python代码:

from langchain_community.llms import Ollama  # 导入ollama包
from langchain_core.prompts import ChatPromptTemplate  # 提示词
from langchain_core.output_parsers import StrOutputParser  # 输出解析器

prompt = ChatPromptTemplate.from_template("写一篇关于{topic}的作文,100字以内。")
model = Ollama(model="deepseek_r1:7b")  # 这里选gpu、cpu负担小的7b模型
output_parser = StrOutputParser()
chain = prompt | model | output_parser  # 组成一条链
result=chain.invoke({"topic": "梅花"})
print(result)

        java代码:

OllamaChatModel model = OllamaChatModel.builder().modelName("deepseek_r1:1.5b").
        baseUrl("http://localhost:11434/").build();
String question = "写一篇关于%s的作文,100字以内。";
Response<AiMessage> result = model.generate(UserMessage.from(String.format(question, "梅花")));
System.out.println(result.content().text());

        代码运行结果如下所示,写一篇关于梅花的作文,100字以内。大模型返回的结果表现还行,还返回它的写作思路。如果大模型的回答显得乱七八糟,不知所云,那么可能得原因是本地部署的模型参数量太小导致。但是不影响我们进行开发测试。

让大模型逐个字返回结果

        上面的代码示例中chain.invoke方法需要等大模型全部返回信息后再返回代程序结果变量中,然后再打印出来。这对于回答信息较少的时候是可以的,但是当大模型需要返回很多信息时,用户需要等待很长时间才能看到结果,这往往是难以接受的。好在langChain中有方便可用的流式返回调用方法让我们程序可以以字符流的形式返回给程序结果变量中。只需要将上述调用大模型的返回方法改成如下形式并输出打印即可。这样用户看到的是程序逐个字符的方式实时响应到展示界面。

        Python代码:

for chunk in chain.stream({"topic": "梅花"}):
    print(chunk,end='') #输出不会自动换行

        java代码:

OllamaStreamingChatModel model = OllamaStreamingChatModel.builder().modelName("deepseek_r1:1.5b").
        baseUrl("http://localhost:11434/").build();
String question = "写一篇关于%s的作文,100字以内。";
model.generate(UserMessage.from(String.format(question, "梅花")), new StreamingResponseHandler<AiMessage>() {
    //生成下一个token时被调用
    @Override
    public void onNext(String token) {
        System.out.print(token);
    }
    //当 LLM 完成生成时:调用 onComplete(Response<T> response)。这里,对于 StreamingChatLanguageModel,
    // T 代表 AiMessage,String 代表 StreamingLanguageModel。 Response 对象包含完整的响应
    @Override
    public void onComplete(Response<AiMessage> response) {
    }
    @Override
    public void onError(Throwable error) {
        error.printStackTrace();
    }
});

让问答在带历史聊天记录中进行

        以上案例只能让你和大模型每次进行对话时都是独立进行的,前面你问过的问题和大模型回答的内容都不会影响下一次对话。这样往往不符合实际应用场景的,比如大多数只能客服系统都需要机器人每次回答时都能知道和用户的历史聊天记录,根据聊天上下文再去回答用户的当前问题。

        这里给出一个对话示例说明。比如用户第一次问:“给出人工智能的定义,请简要说明”,系统回答:“人工智能的定义是......”,然后用户又问:“请详细说明下”,此时系统就要明确用户的问话意图了,根据聊天记录,然后再给出回答才行,否则回答信息可能让用户不知所云。下面给出一个带历史聊天记录的实时问答系统代码示例。

        Python代码:


from langchain_community.llms.ollama import Ollama
from langchain_core.output_parsers import StrOutputParser  # 输出解析器
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.chat_history import BaseChatMessageHistory
from pydantic import BaseModel, Field
from typing import List
from langchain_core.messages import BaseMessage, AIMessage

# 定义一个会话历史类
class InMemoryHistory(BaseChatMessageHistory, BaseModel):
    messages: List[BaseMessage] = Field(default_factory=list)
    def add_messages(self, messages: List[BaseMessage]) -> None:
        """每次Ai回答后将聊天记录下来,构成一个历史集合"""
        self.messages.extend(messages)
    def clear(self) -> None:
        self.messages = []
# 定义一个存储用户会话ID和历史聊天记录的Map集合
store = {}
# 定义一个会话ID回调方法,返回当前会话的历史记录
def get_by_session_id(session_id: str) -> BaseChatMessageHistory:
    if session_id not in store:
        store[session_id] = InMemoryHistory()
    return store[session_id]
# 定义一个会话提示信息模板
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个计算机专家"), # 预设大模型的回答角色
    MessagesPlaceholder(variable_name="history"), # 占位符,用于填充聊天历史
    ("human", "{question}"), # 用户提出的问题
])
# 大模型应答字符串输出流
output_parser = StrOutputParser()
llm = Ollama(model="deepseek_r1:14b")  # ollama管理的大模型
# 创建对话链
chain = prompt | llm | output_parser  # 组成一条链

# 定义带历史聊天记录的调用
chain_with_history = RunnableWithMessageHistory(
    chain, # 调用链
    get_by_session_id, # 会话ID
    input_messages_key="question",
    history_messages_key="history",
)
print("欢迎使用智能问答系统!输入 '退出' 结束对话。")
# 进入用户界面交互循环状态。用户输入,然后系统回答
while True:
    user_input = input("您:") # 等待用户console输入,按enter键确认即可
    # 用户退出问答系统
    if user_input.lower() in ["退出", "exit"]:
        print("感谢使用,再见!")
        break
    else:
        print("AI:", end='')  # 输出不会自动换行
        # 等待大模型流式应答
        for response in chain_with_history.stream({"question": user_input},config={"configurable": {"session_id": "foo"}}): # 这里每次和大模型交互时,固定传入了一个会话ID
            print(response, end='')  # 输出不会自动换行
        print() #换行

        java代码:

// 定义一个用于会话的接口
interface Assistant {
    /**
     *  访问模型接口
     * @param memoryId 用户会话ID
     * @param userMessage 用户输入的问题
     * @return 返回应答流
     */
    TokenStream chat(@MemoryId int memoryId, @dev.langchain4j.service.UserMessage String userMessage);
}

private static void demo() {
    OllamaStreamingChatModel model = OllamaStreamingChatModel.builder().modelName("deepseek_r1:1.5b").
            baseUrl("http://localhost:11434/").timeout(Duration.ofSeconds(60)).build(); // 模型响应最多60秒,防止响应太多信息而耗时太长
    Assistant assistant = AiServices.builder(Assistant.class)
            .streamingChatLanguageModel(model)
            .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10))
            .build();
    System.out.println("欢迎使用智能问答系统!输入 '退出' 结束对话。");
    Scanner scanner = new Scanner(System.in);
    while (true) {
        CountDownLatch latch = new CountDownLatch(1);  // 用于同步用户和AI之间的问答。因为这里模型应答接口是异步执行的
        System.out.print("您:");
        String user_input = scanner.nextLine();
        // 用户退出问答系统
        if (user_input.equalsIgnoreCase("退出") | user_input.equalsIgnoreCase("exit")) {
            System.out.println("感谢使用,再见!");
            break;
        } else {
            // 定义和模型之间的一次交互
            TokenStream tokenStream = assistant.chat(1, user_input);
            System.out.print("AI:");
            // 定义模型应答流接口的事件响应处理逻辑
            tokenStream.onNext((String token) -> {
                System.out.print(token);
            })
                    .onRetrieved((List<Content> contents) -> {
                        System.out.println(contents);
                    })
                    .onComplete((Response<AiMessage> response) -> {
                        latch.countDown();
                        System.out.println();
                    })
                    .onError((Throwable error) -> {
                        // 当前版本如果大模型回答为空内容,则框架里面会报空指针错误。可能跟你的大模型理解能力有关系,正常情况下是不会返回空
                        error.printStackTrace();
                        latch.countDown();
                        System.out.println();
                    })
                    .start(); // 开始调用大模型
            try {
                latch.await(); // 等待大模型返回本次全部应答内容。再循环到用户下次输入
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

        以上代码模拟了用户和大模型问答交互。对话效果如下图所示,在console中打印展示。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值