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
把模型改成本地部署的,试一下多种模型的多种部署方式
参考资料
- langchain官方文档:https://python.langchain.com/docs/tutorials/
- gradio的官方教程:https://modelscope.cn/learn/881?pid=911