【大模型应用】langchain的使用学习记录

0.前言

  demo功能:记录女朋友的要求存入知识库,提问时根据知识库内容进行回答。
  使用Langchain、Gradio、Mongodb、Qwen的API搭建一个demo玩,记录一下过程。

1. langchain基本使用方式

1.1 可以参考的资料

  langchain直接搜资料挺难找到一个合适的比较有逻辑的,要么是大项目,要么可能比较零碎,发现langchain的官方的教程写的很好,叫how-to-guide在这里插入图片描述

1.2 核心组件

(A)LLM问答

  首先先测试一下如何连上LLM,需要先在阿里云上申请一个access_key,这部分基本上是免费的。

from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
## qwen的api_key是sk-开头的那一串
llm = ChatOpenAI(model="qwen-turbo", temperature=0, api_key="sk-xxxx", 
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")

llm.invoke('早上好啊')
## AIMessage(content='早上好!希望您今天过得愉快。有什么我可以帮助您的吗?', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 11, 'total_tokens': 26, 'completion_tokens_details': None, 'prompt_tokens_details': {'audio_tokens': None, 'cached_tokens': 0}}, 'model_name': 'qwen-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None}, id='run-f60d3f8a-83d7-4b74-87a9-b8f972847798-0', usage_metadata={'input_tokens': 11, 'output_tokens': 15, 'total_tokens': 26, 'input_token_details': {'cache_read': 0}, 'output_token_details': {}})

(B) extraction

  意图识别/信息提取,可以从用户输入的prompt来提取出用户到底想干什么,或者总结一句话里面的内容要点,一开始想做一些结构化的解析后续用于后面本地模型的训练微调。这里使用了tool call的方式,让模型对着几个函数的入参找到最匹配的函数,做tool choice。
  我的输入可能会有3种形式:(1)在某个场景下,问女朋友倾向于做什么,女朋友回答希望A,很讨厌B;(2)在某个场景下,问女朋友倾向于做什么,女朋友回答希望A;(3)在这个场景应该带女朋友干些啥呢?
  这三种形式的输入分别可以解析出结构化的信息:(1)场景+希望的回答+讨厌的回答;(2)场景+希望的回答;(3)场景
  相对应的,用3个函数来实现这3种形式的解析和执行动作,对于(1)和(2),解析出来把信息存起来可以作为知识库,对于(3)是要对这个问题进行回答的。3个函数拥有不同的函数参数,每个参数有不同的描述,LLM模型会对prompt进行识别,判断当前的输入和哪个函数最匹配

### 形式1,提取出场景+希望的回答+讨厌的回答
### src_input是一个自定义的injected的参数,不需要模型提取,是代码里面传入的用于记录原始的prompt的
@tool(parse_docstring=True)
def extract_accepted_rejected(
    context: str, accepted_answer: str, rejected_answer: str, src_input: Annotated[str, InjectedToolArg]
) -> None:
    """从输入中提取上下文问题信息和女朋友喜欢的答案。

    Args:
        context: 场景、问题信息,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面就问她和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出主体上下文里面的场景问题"今天早上起来问女朋友想吃什么早餐"。
        accepted_answer: 女朋友喜欢的答案,把女朋友喜欢的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"吃鹿亚平胡辣汤"。
        rejected_answer: 女朋友讨厌的答案,把女朋友讨厌的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"不喜欢和府捞面"。
    
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    current_dict['accepted_answer'] = accepted_answer
    current_dict['rejected_answer'] = rejected_answer
    write_to_mongodb(current_dict)
    show_message = "Tool call 'extract_accepted_rejected': 'context'=" + context + "," + "'accepted_answer'=" + accepted_answer + "," + "'rejected_answer'=" + rejected_answer
    update_message(show_message)


### 形式2,提取出场景+希望的回答
@tool(parse_docstring=True)
def extract_accepted(
    context: str, accepted_answer: str, src_input: Annotated[str, InjectedToolArg]
) -> None:
    """从输入中提取场景问题信息和女朋友喜欢的答案。

    Args:
        context: 场景、问题信息,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面就问她和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出主体上下文里面的场景问题"今天早上起来问女朋友想吃什么早餐"。
        accepted_answer: 女朋友喜欢的答案,把女朋友喜欢的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"吃鹿亚平胡辣汤"。
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    current_dict['accepted_answer'] = accepted_answer
    current_dict['rejected_answer'] = ""
    write_to_mongodb(current_dict)  # 修改为写入 MongoDB
    show_message = "Tool call 'extract_accepted': 'context'=" + context + "," + "'accepted_answer'=" + accepted_answer + ""
    update_message(show_message)

### 形式3,提取出场景
### 只有一个参数context来自于用户输入
@tool(parse_docstring=True)
def extract_question(
    context: str,src_input: Annotated[str, InjectedToolArg]
) -> None:
    """判断是否是来自用户的询问,并提取出询问的内容

    Args:
        context:问题信息,例如输入是"快到中午了,该和女朋友去吃什么呢",需要提取出的问题信息是"中午该和女朋友吃什么"
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    show_message = "Tool call 'extract_question': 'context'="+context
    update_message(show_message)

(C) tool call chain

  识别到和哪个函数匹配后,要执行这个函数,需要把函数tool进行绑定,然后拼成一个chain让langchain来从头到尾执行这个chain。

tools = [
    extract_question,
    extract_accepted,
    extract_accepted_rejected
]
llm_with_tools = extract_llm.bind_tools(tools)

from copy import deepcopy
from langchain_core.runnables import chain


@chain
def inject_user_favor(ai_msg):
    tool_calls = []
    for tool_call in ai_msg.tool_calls:
        tool_call_copy = deepcopy(tool_call)
        tool_call_copy["args"]["src_input"] = src_input  # 这个是injected的参数,不需要模型提取的
        tool_calls.append(tool_call_copy)
    return tool_calls

@chain
def tool_router(tool_call):
    return tool_map[tool_call["name"]]

tool_map = {tool.name: tool for tool in tools}
chain = llm_with_tools | inject_user_favor | tool_router.map()  # 拼成一个chain

  后续使用可以直接调用这个chain,例如

src_input = "中午好饿,我问女朋友想吃什么午饭呢,女朋友说不想吃粉了,这种情况下她喜欢吃佬肥猫家的牛蛙"
b=chain.invoke(src_input)

### b是一个ToolMessage,name为extract_accepted,说明LLM认为extract_accepted的参数和prompt最匹配,不止是匹配,langchain同时还会执行extract_accepted这个函数,例如可以把数据存到数据库里面
[ToolMessage(content='null', name='extract_accepted', tool_call_id='call_e21dee554b6246ba8fbb3a')]

(D)从数据库取回相似材料

  如果要模型参考一些资料进行回答而不是凭空回答,可以让模型每次先从数据库里面找到相似的材料,然后基于材料回答问题。数据存储可以各种方式,
  langchain提供了找相似材料的方法:计算用户输入的句子向量表示,计算数据库里面数据的向量表示,然后计算用户输入的句子和数据库每条数据的相似度,按照相似度进行排序。
  在这一部分,需要使用embedding模型进行句子向量编码,阿里的是DashScopeEmbeddings,对于一个句子,返回的是大小1024的向量,具体参考通义的官网文档

df = pd.DataFrame('数据库.csv')

src_text = []
for i in range(len(df)):
    src_text.append(df.loc[i, 'src_input'])
    
from langchain_community.embeddings import DashScopeEmbeddings
## 通义的文本embedding模型需要使用DashScopeEmbeddings,不同公司的大模型的导入是不一样的
## 具体可以去langchain官网或者模型官网查:https://python.langchain.com/docs/integrations/text_embedding/
embed_model = DashScopeEmbeddings(model="text-embedding-v3", dashscope_api_key="sk-xxxxx")
client = OpenAI(
    api_key="sk-xxxx", ## 通义的sk开头的key
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

from langchain_core.vectorstores import InMemoryVectorStore
vectorstore = InMemoryVectorStore.from_texts(
    texts = src_text, embedding=embed_model
)

# Use the vectorstore as a retriever
retriever = vectorstore.as_retriever()

retrieved_documents = retriever.invoke('明天早上吃什么好呢')
# 会返回和'明天早上吃什么好呢'最相近的资料,如果不指定下标[0],返回的是按照相似度排序的结果list
ref = retrieved_documents[0].page_content  

在这里插入图片描述

2. 前后端组件

  在这部分发现有一个参考资料很清晰,魔搭社区官方写的gradio教程,改一改就能用。同时可以VSCODE上再装上通义灵码的免费extention,通义灵码也可以自己写gradio的太强了,几行代码就可以完事。数据存储的mongodb这部分也可以直接让通义灵码来写,有Ai developer模式直接自动修改源文件。最后再询问它如何启动mongodb以及启动顺序。

3. 完整代码

  运行时先启动mongodb,然后在vscode运行。
在这里插入图片描述

######################### part1 创建知识库 #########################
#################################################################################
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI
from openai import OpenAI

import json
from typing import List
from langchain_core.tools import InjectedToolArg, tool
from typing_extensions import Annotated

import pandas as pd
from pymongo import MongoClient

# 假设 MongoDB 连接字符串和数据库名称
mongo_uri = "mongodb://localhost:27017/"
db_name = "my_girl_friend_finder_v3"
collection_name = "20250207_1"

# 创建 MongoDB 客户端和数据库连接
client = MongoClient(mongo_uri)
db = client[db_name]
collection = db[collection_name]

global_message = ""
src_input = "" # 用于记录当前用户的prompt

def update_message(show_message):
    global global_message
    global_message = show_message

def show_message():
    global global_message
    return global_message

@tool(parse_docstring=True)
def extract_question(
    context: str,src_input: Annotated[str, InjectedToolArg]
) -> None:
    """判断是否是来自用户的询问,并提取出询问的内容

    Args:
        context:问题信息,例如输入是"快到中午了,该和女朋友去吃什么呢",需要提取出的问题信息是"中午该和女朋友吃什么"
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    show_message = "Tool call 'extract_question': 'context'="+context
    update_message(show_message)


def write_to_mongodb(data):
    """将数据写入 MongoDB"""
    global collection
    collection.insert_one(data)


@tool(parse_docstring=True)
def extract_accepted(
    context: str, accepted_answer: str, src_input: Annotated[str, InjectedToolArg]
) -> None:
    """从输入中提取场景问题信息和女朋友喜欢的答案。

    Args:
        context: 场景、问题信息,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面就问她和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出主体上下文里面的场景问题"今天早上起来问女朋友想吃什么早餐"。
        accepted_answer: 女朋友喜欢的答案,把女朋友喜欢的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"吃鹿亚平胡辣汤"。
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    current_dict['accepted_answer'] = accepted_answer
    current_dict['rejected_answer'] = ""
    write_to_mongodb(current_dict)  # 修改为写入 MongoDB
    show_message = "Tool call 'extract_accepted': 'context'=" + context + "," + "'accepted_answer'=" + accepted_answer + ""
    update_message(show_message)


@tool(parse_docstring=True)
def extract_accepted_rejected(
    context: str, accepted_answer: str, rejected_answer: str, src_input: Annotated[str, InjectedToolArg]
) -> None:
    """从输入中提取上下文问题信息和女朋友喜欢的答案。

    Args:
        context: 场景、问题信息,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面就问她和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出主体上下文里面的场景问题"今天早上起来问女朋友想吃什么早餐"。
        accepted_answer: 女朋友喜欢的答案,把女朋友喜欢的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"吃鹿亚平胡辣汤"。
        rejected_answer: 女朋友讨厌的答案,把女朋友讨厌的答案提取出来,例如输入是"今天早上起来问女朋友想吃什么早餐,我喜欢和府捞面问和府捞面行不行,女朋友不喜欢和府捞面很生气,说要吃鹿亚平胡辣汤",需要提取出"不喜欢和府捞面"。
    
    """
    current_dict = {}
    current_dict['src_input'] = src_input
    current_dict['context'] = context
    current_dict['accepted_answer'] = accepted_answer
    current_dict['rejected_answer'] = rejected_answer
    write_to_mongodb(current_dict)
    show_message = "Tool call 'extract_accepted_rejected': 'context'=" + context + "," + "'accepted_answer'=" + accepted_answer + "," + "'rejected_answer'=" + rejected_answer
    update_message(show_message)

extract_llm = ChatOpenAI(model="qwen-turbo", temperature=0, api_key="sk-xxxxx", 
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")

tools = [
    extract_question,
    extract_accepted,
    extract_accepted_rejected
]
llm_with_tools = extract_llm.bind_tools(tools)


from copy import deepcopy
from langchain_core.runnables import chain


@chain
def inject_user_favor(ai_msg):
    tool_calls = []
    for tool_call in ai_msg.tool_calls:
        tool_call_copy = deepcopy(tool_call)
        tool_call_copy["args"]["src_input"] = src_input
        tool_calls.append(tool_call_copy)
    return tool_calls


@chain
def tool_router(tool_call):
    return tool_map[tool_call["name"]]


tool_map = {tool.name: tool for tool in tools}
chain = llm_with_tools | inject_user_favor | tool_router.map()


## 模拟知识库,这些是需要输入到知识库的内容
# #原始的prompt输入
src_input = "今天早上起来,我问女朋友想吃什么早餐呢,因为我喜欢和府捞面,所以问她和府捞面行不行,女朋友不喜欢和府捞面很生气还骂了我一顿,说就要去喝鹿亚平胡辣汤"
a = chain.invoke(src_input)


# # 原始的prompt输入
src_input = "中午好饿,我问女朋友想吃什么午饭呢,女朋友说不想吃粉了,这种情况下她喜欢吃佬肥猫家的牛蛙"
b=chain.invoke(src_input)


# # 原始的prompt输入
src_input = "晚上不知道吃啥,问女朋友,因为周六晚上达美乐7折,女朋友想吃达美乐"
c=chain.invoke(src_input)


########################### part2 知识库检索 ###########################
#################################################################################
# 读取知识库
cursor = collection.find()
df = pd.DataFrame(list(cursor))

src_text = []
for i in range(len(df)):
    src_text.append(df.loc[i, 'src_input'])
    
from langchain_community.embeddings import DashScopeEmbeddings
embed_model = DashScopeEmbeddings(model="text-embedding-v3", dashscope_api_key="sk-xxxxx")

client = OpenAI(
    api_key="sk-xxxxx", 
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

from langchain_core.vectorstores import InMemoryVectorStore
vectorstore = InMemoryVectorStore.from_texts(
    texts = src_text, embedding=embed_model
)

# Use the vectorstore as a retriever
retriever = vectorstore.as_retriever()



######################### part3 gradio前后端搭建 ##############################
#################################################################################
import gradio as gr  # 导入 Gradio 库
from openai import OpenAI
import os 

# 仅在 Notebook 中运行时需要,本地运行无需该部分:检查是否已存在名为 `app` 的 Gradio 应用
try:
    app.close()  # 如果 `app` 存在,则关闭它
except NameError:
    # 如果 `app` 不存在,捕获 NameError 异常,并忽略该异常
    pass

def gen_answer(client,retrieved_documents,src_input):
    ref = retrieved_documents[0].page_content
    prompt = "你是一个专属生活小助手,请在参考信息后进行回答,不要解释。参考信息为:\n"+ref+"\n"+"问题为:"+src_input+"\n"+"请在参考信息后,直接给出答案。"
    ans = client.chat.completions.create(
        model="qwen-turbo",
        messages=[
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": prompt},
        ],
        stream=True,
    )
    return ans

# 定义聊天函数,支持历史消息处理和流式输出
def chat(prompt, messages):
    global global_message
    # 打印当前的聊天记录
    print(messages)

    # 将用户的输入添加到消息记录中,带历史消息的
    messages.append({'role': 'user', 'content': prompt})
    src_input = prompt
    # 使用 yield 返回初始状态以开始流式响应
    yield '', messages

    try:
        # 逐步处理来自流响应的数据
        now_calls = chain.invoke(src_input)
        full_response = ""  # 初始化完整响应
        messages.append({'role': 'assistant', 'content': full_response})  # 为助手的响应添加占位
        if now_calls[0].name=='extract_question': # 如果是提问,提取出问题,并检索参考资料
            retrieved_documents = retriever.invoke(src_input)
            ref = retrieved_documents[0].page_content
            prompt = "你是一个专属生活小助手,请在参考信息后进行回答,不要解释。参考信息为:\n"+ref+"\n"+"问题为:"+src_input+"\n"+"请在参考信息后,直接给出答案。"
            ans = client.chat.completions.create(
                model="qwen-turbo",
                messages=[
                    {"role": "system", "content": "You are a helpful assistant."},
                    {"role": "user", "content": prompt},
                ],
                stream=True,temperature=0
            )
            answers = ""
            # 流式输出
            for chunk in ans:
                content = chunk.choices[0].delta.content
                answers += content
                messages[-1]['content'] = answers  # 更新历史记录中的助手内容
                # 使用 yield 实时更新使用界面的显示
                yield '', messages
        else:
            # 如果不是提问,只是调用大模型增加知识库的信息,只需要让messages在前端显示调用了哪个function即可
            # 非流式显示global_message
            messages[-1]['content'] = global_message
            yield '', messages
    except Exception as e:
            # 如果请求失败,捕获异常并返回错误信息
        yield str(e)

# 创建 Gradio 应用程序
with gr.Blocks() as app:  # 使用 Gradio 的 Blocks 创建新的应用
    gr.Markdown("# Chat with history")  # 使用 Markdown 添加标题
    chatbot = gr.Chatbot(type="messages")  # 创建一个聊天机器人组件,用于显示消息历史
    input = gr.Textbox(show_label=False)  # 创建一个文本框组件,用于输入用户信息

    # 当用户提交输入时,调用 `chat` 函数,并实时更新聊天记录
    input.submit(fn=chat, inputs=[input, chatbot], outputs=[input, chatbot])

# 启动 Gradio 应用程序,设置服务器端口为 7861
app.launch(server_port=7861)

TODO

  把模型改成本地部署的,试一下多种模型的多种部署方式

参考资料

  1. langchain官方文档:https://python.langchain.com/docs/tutorials/
  2. gradio的官方教程:https://modelscope.cn/learn/881?pid=911
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值