【大模型应用】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(