LangChain 工程架构解析

avatar
@哈啰

LangChain是什么

LangChain 是一个基于开源大语言模型的 AI 工程开发框架,旨在使研究人员和开发人员能够更轻松地构建、实验和部署以自然语言处理(NLP)为中心的应用程序。它提供了多种组件和工具,可帮助用户利用最近的语言模型进展,如大型 Transformer 模型等,并且可以与 Hugging Face 等平台集成。LangChain 的核心理念是将语言模型用作协作工具,通过它,开发者可以构建出处理复杂任务的系统,并且可以高效地对接不同的数据源和应用程序接口(APIs)。

技术架构

image.png

如图,从下至上分别是:

  • LangChain 库:Python 和 JavaScript 库,包含接口和集成,用于各种组件的组合,以及现成的链和代理的实现。
  • LangChain 模板:易于部署的各种任务的参考架构集合。
  • LangServe:将 LangChain 链部署为 REST API 的库。
  • LangSmith:开发平台,可用于调试、测试、评估和监控基于任何 LLM 框架构建的链,并与 LangChain 无缝集成。

LangChain 库

LangChain 的核心组件和能力(六大核心抽象,用于构建复杂的AI应用,同时保持了良好的扩展能力。)

  • Models(模型):
    Chat Models(聊天模型): LangChain 提供了一个标准接口,支持聊天模型。这些模型是语言模型的变体,其接口基于消息而不是原始文本。您可以使用不同类型的消息(如 AIMessage、HumanMessage、SystemMessage 和 ChatMessage)与聊天模型进行交互。
    Embeddings(嵌入): 帮助将文本转换为向量表示,以便在应用程序中进行处理。
    LLMs(大型语言模型): LangChain 支持大型语言模型,例如 ChatGPT,用于各种自然语言处理任务。
  • Prompts(提示):
    Prompt Templates(提示模板): 提供易于部署的参考架构,适用于不同任务。
  • Indexes(索引):
    Vector Databases(向量数据库): 用于存储和检索向量表示,例如文本嵌入。
    Interaction Layer Prompts(交互层提示): 用于构建用户与模型之间的交互。
  • Memory(记忆):
    External Knowledge(外部知识): 整合外部知识源,丰富模型的知识库。
    External Tools(外部工具): 与外部环境进行交互,例如通过 API 请求执行操作。
  • Chains(链):
    LangChain提供了LCEL(LangChain Expression Language)声明式编程语言,将不同组件组合起来解决特定任务,例如在大量文本中查找信息。
  • Agents(代理):
    使得 LLMs 能够与外部环境进行交互,例如通过 API 请求执行操作。

总之,LangChain 是一个强大的工具箱,不仅涵盖了基础工具,还为个性化需求提供了自定义组件解决方案。它使开发者能够更专注于创新和优化产品功能,从原型到生产环境的转化变得更加高效。

使用例子

最原始的RESTful API -> LangChain-OpenAI SDK

import os
import requests

# API Key
api_key = os.getenv('OPENAI_API_KEY')

# 头部信息
headers = {
    'Content-Type': 'application/json',
    'Authorization': f'Bearer {api_key}'
}

# 准备数据
data = {
    'model': 'gpt-4',
    'messages': [{'role': 'user', 'content': '什么是图计算?'}],
    'temperature': 0.7
}

# 调用API
url = 'https://api.openai.com/v1/chat/completions'
response = requests.post(url, json=data, headers=headers)
answer = response.json()['choices'][0]['message']['content']
print(answer)
from langchain_openai import ChatOpenAI

# 调用Chat Completion API
llm = ChatOpenAI(model_name='gpt-4')
response = llm.invoke('什么是图计算?')
print(response)

IO抽象

image.png

对于文本生成模型服务来说,实际的输入和输出本质上都是字符串,因此直接裸调用LLM服务带来的问题是要在输入格式化和输出结果解析上做大量的重复的文本处理工作。LangChain当然考虑到这一点,提供了Prompt和OutputParser抽象,用户可以根据自己的需要选择具体的实现类型使用。

from langchain_openai import ChatOpenAI

# 调用Chat Completion API
llm = ChatOpenAI(model_name='gpt-4')
response = llm.invoke('什么是图计算?')
print(response)
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# 创建LLM
llm = ChatOpenAI(model_name='gpt-4')

# 创建Prompt
prompt = ChatPromptTemplate.from_template("{question}")

# 创建输出解析器
output_parser = StrOutputParser()

# 调用LLM
message = prompt.invoke({'question': '什么是图计算?'})
response = llm.invoke(message)
answer = output_parser.invoke(response)
print(answer)

组装成链

1.LCEL
LangChain的表达式语言(LCEL)通过重载__or__运算符的思路,构建了类似Unix管道运算符的设计,实现更简洁的LLM调用形式。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

# 创建LLM
llm = ChatOpenAI(model_name='gpt-4')

# 创建Prompt
prompt = ChatPromptTemplate.from_template("{question}")

# 创建输出解析器
output_parser = StrOutputParser()

# 调用LLM
message = prompt.invoke({'question': '什么是图计算?'})
response = llm.invoke(message)
answer = output_parser.invoke(response)
print(answer)
# 创建Chain
chain = prompt | llm | output_parser

# 调用Chain
answer = chain.invoke({'question': '什么是图计算?'})
print(answer)

2.RunnablePassthrough
为了简化Chain的参数调用格式,也可以借助RunnablePassthrough透传上游参数输入。

from langchain_core.runnables import RunnablePassthrough

# 创建Chain
chain = {"question": RunnablePassthrough()} | prompt | llm | output_parser

# 调用Chain
answer = chain.invoke('什么是图计算?')
print(answer)

3.DAG
代码示例输出:苹果是一种营养丰富的水果,具有帮助消化、保护心脏、降低糖尿病风险、强化免疫系统、帮助减肥、保护视力、预防哮喘、抗癌和提升记忆力等多种好处。然而,过度食用或者不适当的食用方式也可能带来一些不利影响,如引发过敏、导致腹泻、对牙齿造成伤害、可能携带农药残留、影响正常饮食和钙质吸收、增加蛀牙风险和引发胃痛等。因此,我们在享受苹果带来的好处的同时,也需要注意适量和正确的食用方式。

from operator import itemgetter

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import ChatOpenAI

# 创建LLM
llm = ChatOpenAI(model_name='gpt-4')

# 创建输出解析器
output_parser = StrOutputParser()

# 创建Prompt
topic_prompt = ChatPromptTemplate.from_template("生成一种'{input}'的名称")
good_prompt = ChatPromptTemplate.from_template("列举{topic}的好处:")
bad_prompt = ChatPromptTemplate.from_template("列举{topic}的坏处:")
summary_prompt = ChatPromptTemplate.from_messages(
    [
        ("ai", "{topic}"),
        ("human", "好处:\n{good}\n\n坏处:\n{bad}"),
        ("system", "生成最终结论"),
    ]
)

# 创建组合Chain
topic_chain = topic_prompt | llm | output_parser | {"topic": RunnablePassthrough()}
goods_chain = good_prompt | llm | output_parser
bads_chain = bad_prompt | llm | output_parser
summary_chain = summary_prompt | llm | output_parser
chain = (
    topic_chain
    | {
        "good": goods_chain,
        "bad": bads_chain,
        "topic": itemgetter("topic"),
    }
    | summary_chain
)

# 调用chain
answer = chain.invoke({"input": '常见水果'})
print(answer)

4.LangSmith

image.png

5.LangGraph
基于LCEL确实能描述比较复杂的LangChain计算图结构,但依然有DAG天然的设计限制,即不能支持“循环”。于是LangChain社区推出了一个新的项目——LangGraph,期望基于LangChain构建支持循环和跨多链的计算图结构,以描述更复杂的,甚至具备自动化属性的AI工程应用逻辑,比如智能体应用。其具体使用方式可以参考LangGraph文档。

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage
from langgraph.graph import END, MessageGraph

# 初始化聊天模型
model = ChatOpenAI(temperature=0)

# 创建一个 MessageGraph
graph = MessageGraph()

# 添加一个名为 "oracle" 的节点,它执行聊天模型并返回结果
graph.add_node("oracle", model)
graph.add_edge("oracle", END)
graph.set_entry_point("oracle")

# 编译图
runnable = graph.compile()

# 运行图
result = runnable.invoke(HumanMessage("What is 1 + 1?"))
print(result)  # 输出聊天模型的回答

Memory(记忆)

通过Chain,LangChain相当于以“工作流”的形式,将LLM与IO组件进行了有秩序的连接,从而具备构建复杂AI工程流程的能力。而我们都知道LLM提供的文本生成服务本身不提供记忆功能,需要用户自己管理对话历史。因此引入Memory组件,可以很好地扩展AI工程的能力边界。

image.png

RAG(检索增强)

拥有记忆后,确实扩展了AI工程的应用场景。但是在专有领域,LLM无法学习到所有的专业知识细节,因此在面向专业领域知识的提问时,无法给出可靠准确的回答,甚至会“胡言乱语”,这种现象称之为LLM的“幻觉”。检索增强生成(RAG)把信息检索技术和大模型结合起来,将检索出来的文档和提示词一起提供给大模型服务,从而生成更可靠的答案,有效的缓解大模型推理的“幻觉”问题。

image.png

相比提示词工程,RAG有更丰富的上下文和数据样本,可以不需要用户提供过多的背景描述,即能生成比较符合用户预期的答案。相比于模型微调,RAG可以提升问答内容的时效性和可靠性,同时在一定程度上保护了业务数据的隐私性。

但由于每次问答都涉及外部系统数据检索,因此RAG的响应时延相对较高。另外,引用的外部知识数据会消耗大量的模型Token资源。因此,用户需要结合自身的实际应用场景做合适的技术选型。

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.vectorstores.faiss import FAISS
from langchain_core.documents import Document
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

# 创建LLM
llm = ChatOpenAI(model_name='gpt-4')

# 创建Prompt
prompt = ChatPromptTemplate.from_template('基于上下文:{context}\n回答:{input}')

# 创建输出解析器
output_parser = StrOutputParser()

# 模拟文档
docs = [Document(page_content="TuGraph是蚂蚁开源的图数据库产品")]

# 文档嵌入
splits = RecursiveCharacterTextSplitter().split_documents(docs)
vector_store = FAISS.from_documents(splits, OpenAIEmbeddings())
retriever = vector_store.as_retriever()

# 创建Chain
chain_no_context = RunnablePassthrough() | llm | output_parser
chain = (
    {"context": retriever, "input": RunnablePassthrough()}
    | prompt | llm | output_parser
)

# 调用Chain
print(chain_no_context.invoke('蚂蚁图数据库开源了吗?'))
print(chain.invoke('蚂蚁图数据库开源了吗?'))

image.png

结合示例和向量数据库的存取过程,我们简单理解一下RAG中关键组件:

  • DocumentLoader:从外部系统检索文档数据。简单起见,示例中直接构造了测试文档对象。实际上LangChain提供了文档加载器BaseLoader的接口抽象和大量实现,具体可根据自身需要选择使用。
  • TextSplitter:将文档分割成块,以适应大模型上下文窗口。示例中采用了常用的RecursiveCharacterTextSplitter,其他参考LangChain的TextSplitter接口和实现。
  • EmbeddingsModel:文本嵌入模型,提供将文本编码为向量的能力。文档写入和查询匹配前都会先执行文本嵌入编码。示例采用了OpenAI的文本嵌入模型服务,其他参考LangChain的Embeddings接口和实现。
  • VectorStore:向量存储,提供向量存储和相似性检索(ANN算法)能力。LangChain支持的向量存储参考VectorStore接口和实现。示例采用了Meta的Faiss向量数据库。
  • Retriever:向量存储的查询器。一般和VectorStore配套实现,通过as_retriever方法获取,LangChain提供的Retriever抽象接口是BaseRetriever。

Tool(插件)

“会使用工具”是人类和动物的根本区别。

要构建更强大的AI工程应用,只有生成文本这样的“纸上谈兵”能力自然是不够的。工具不仅仅是“肢体”的延伸,更是为“大脑”插上了想象力的“翅膀”。借助工具,才能让AI应用的能力真正具备无限的可能,才能从“认识世界”走向“改变世界”。

这里不得不提到OpenAI的Chat Completion API提供的函数调用能力(注意这里不是Assistant的函数调用),通过在对话请求内附加tools参数描述工具的定义格式(原先的functions参数已过期),LLM会根据提示词推断出需要调用哪些工具,并提供具体的调用参数信息。用户需要根据返回的工具调用信息,自行触发相关工具的回调。下一章内容我们可以看到工具的调用动作可以通过Agent自主接管。

image.png

from openai import OpenAI
import json

client = OpenAI()

# Example dummy function hard coded to return the same weather
# In production, this could be your backend API or an external API
def get_current_weather(location, unit="fahrenheit"):
    """Get the current weather in a given location"""
    if "tokyo" in location.lower():
        return json.dumps({"location": "Tokyo", "temperature": "10", "unit": unit})
    elif "san francisco" in location.lower():
        return json.dumps({"location": "San Francisco", "temperature": "72", "unit": unit})
    elif "paris" in location.lower():
        return json.dumps({"location": "Paris", "temperature": "22", "unit": unit})
    else:
        return json.dumps({"location": location, "temperature": "unknown"})

def run_conversation():
    # Step 1: send the conversation and available functions to the model
    messages = [{"role": "user", "content": "What's the weather like in San Francisco, Tokyo, and Paris?"}]
    tools = [
        {
            "type": "function",
            "function": {
                "name": "get_current_weather",
                "description": "Get the current weather in a given location",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "location": {
                            "type": "string",
                            "description": "The city and state, e.g. San Francisco, CA",
                        },
                        "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]},
                    },
                    "required": ["location"],
                },
            },
        }
    ]
    response = client.chat.completions.create(
        model="gpt-3.5-turbo-0125",
        messages=messages,
        tools=tools,
        tool_choice="auto",  # auto is default, but we'll be explicit
    )
    response_message = response.choices[0].message
    tool_calls = response_message.tool_calls
    # Step 2: check if the model wanted to call a function
    if tool_calls:
        # Step 3: call the function
        # Note: the JSON response may not always be valid; be sure to handle errors
        available_functions = {
            "get_current_weather": get_current_weather,
        }  # only one function in this example, but you can have multiple
        messages.append(response_message)  # extend conversation with assistant's reply
        # Step 4: send the info for each function call and function response to the model
        for tool_call in tool_calls:
            function_name = tool_call.function.name
            function_to_call = available_functions[function_name]
            function_args = json.loads(tool_call.function.arguments)
            function_response = function_to_call(
                location=function_args.get("location"),
                unit=function_args.get("unit"),
            )
            messages.append(
                {
                    "tool_call_id": tool_call.id,
                    "role": "tool",
                    "name": function_name,
                    "content": function_response,
                }
            )  # extend conversation with function response
        second_response = client.chat.completions.create(
            model="gpt-3.5-turbo-0125",
            messages=messages,
        )  # get a new response from the model where it can see the function response
        return second_response
print(run_conversation())
import random

from langchain_core.output_parsers.openai_tools import JsonOutputToolsParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI


# 定义Tool
@tool
def get_temperature(city: str) -> int:
    """获取指定城市的当前气温"""
    return random.randint(-20, 50)


# 创建LLM
llm = ChatOpenAI(model_name='gpt-4')

# 创建JSON输出解析器
output_parser = JsonOutputToolsParser()

# 创建Chain
chain = (
    RunnablePassthrough()
    | llm.bind_tools(tools=[get_temperature])
    | output_parser
)

# 调用Chain
print(chain.invoke('杭州今天多少度?'))

代码示例输出:

[{'type': 'get_temperature', 'args': {'city': '杭州'}}]

Agent(智能体)

Agent的核心思想是使用大型语言模型(LLM)来选择要采取的行动序列。在Chain中行动序列是硬编码的,而Agent则采用语言模型作为推理引擎来确定以什么样的顺序采取什么样的行动。Agent相比Chain最典型的特点是“自治”,它可以通过借助LLM专长的推理能力,自动化地决策获取什么样的知识,采取什么样的行动,直到完成用户设定的最终目标。

image.png 因此,作为一个智能体,需要具备以下核心能力:

  • 规划:借助于LLM强大的推理能力,实现任务目标的规划拆解和自我反思。
  • 记忆:具备短期记忆(上下文)和长期记忆(向量存储),以及快速的知识检索能力。
  • 行动:根据拆解的任务需求正确地调用工具以达到任务的目的。
  • 协作:通过与其他智能体交互合作,完成更复杂的任务目标。
import random

from langchain.agents import create_openai_tools_agent, \
    AgentExecutor
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder, \
    HumanMessagePromptTemplate, SystemMessagePromptTemplate
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI

# 创建LLM
llm = ChatOpenAI()

# 定义Tool
@tool
def get_temperature(city: str) -> int:
    """获取指定城市的当前气温"""
    return random.randint(-20, 50)


# 创建Agent提示词模板
prompt = ChatPromptTemplate.from_messages([
    SystemMessagePromptTemplate.from_template('You are a helpful assistant'),
    MessagesPlaceholder(variable_name='chat_history', optional=True),
    HumanMessagePromptTemplate.from_template('{input}'),
    MessagesPlaceholder(variable_name='agent_scratchpad')
])

# 创建Agent
tools = [get_temperature]
agent = create_openai_tools_agent(llm, tools, prompt=prompt)

# 执行Agent
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
print(agent_executor.invoke({'input': '今天杭州多少度?'})['output'])

(本文作者:方鑫)