假如你和我一样在准备24年的春招,在前端全栈外,再准备一些AI的内容是非常有必要的。24年是AI红利年,AIGC+各种岗位大厂机会会多些,同意的请点赞。也欢迎朋友们加我微信shunwuyu, 一起交流。
前言
在上篇文章LangGraph 进化LangChain到多代理运行时 - 掘金 (juejin.cn)中,我们编写了一个组合search engine
和twitter writter engine
的例子。LangChain
新推出的核心机制LangGraph
,以多代理的方式,把活给干完了。今天,我们将细致学习使用到的代码。请打开LangChain-Tutorials/langgraph_nodes_edges.ipynb at main · sugarforever/LangChain-Tutorials (github.com)
代码
我们将在colab调试我们的代码,colab是Google推出的在线机器学习平台,除了一些模型训练、比较费GPU的任务外,我的学习型demo都可以在这里free跑起来。
安装依赖
css
!pip install -q -U langchain langchain_openai langgraph google-search-results
-
首先
!
是什么意思呢?pip install
是安装依赖包,我们在写python的时候,好像前面没有加过!
。这里的!
是指在特定交互环境中(如 Jupyter Notebook、JupyterLab 或 Google Colab)的命令执行标记。用于告诉该环境应当以系统shell来执行紧跟在其后的命令,而不是将其当作Python代码来执行。 -
-q -U 是什么意思
-q 只显示 错误信息,如果没有,就不显示,让安装以安静模式运行 -U update 表示安装最新版本的包
-
langchain_openai
我们在玩node的时候,会到npmjs.org去查看包的详细信息。当我们用pip安装包的时候,可以到pypi.org/,去看。
langchain
框架负责核心模块,langchain_openai
为LangChain
集成了OpenAI
SDK的调用。langgraph
是新功能,为langchain
带来了多代理的编程方式google-search-results
来自serapi -
注册serapi, 并拿到sdk
我们在开发生成式应用时,多次使用到serapi先从google搜索中拿到数据,再去生成。
- 环境变量设置
csharp
import os # os 模块提供了与操作系统相关的接口, 比如文件、目录,环境变量、进程管理等 这里主要使用环境变量
from google.colab import userdata
os.environ['SERPAPI_API_KEY'] = userdata.get('GOOGLE_API_KEY') os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY') os.environ["LANGCHAIN_TRACING_V2"] = "true"
# 项目名
os.environ["LANGCHAIN_PROJECT"] = "LangGraph"
# LANGCHAIN_API_KEY
os.environ["LANGCHAIN_API_KEY"] = userdata.get('LANGSMITH_API_KEY')
colab 为我们提供了userdata, 统一存放OPENAI_API_KEY
、GOOGLE_API_KEY
等,我们可以在任一notebook里根据key取出userData里的值。
-
LangSmith
是一个用于构建生产级 LLM 应用程序的平台,用于交付,可以来到LangSmith注册拿到key, 它的使用我们后续开新的文章再细讲。
-
搜索试运行
java
from langchain_community.utilities import SerpAPIWrapper
search = SerpAPIWrapper()
search.run("Obama's first name?")
langchain_community
是langchain
和一些第三方常用库结合的模块。当我们安装了langchain
、google-search-results
后,我们就可以调用langchain_community.utilities
提供的SerpAPIWrapper
来做搜索工作。可以这样理解,langchain_community.utilities
将第三方工具封装得更适合langchain
业务,比如在上面的代码里,实例化后的search是可以run的。所以,我们有langchain-opnai, 有SerpAPIWrapper, 第三方工具库由langchain_community.utilities
提供。
Barack Hussein Obama II
- 引入相应的模块
python
import functools, operator, requests, os, json
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_core.messages import BaseMessage, HumanMessage
from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langgraph.graph import StateGraph, END
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from typing import Annotated, Any, Dict, List, Optional, Sequence, TypedDict
首先,我们在标准库中引入functools, 它提供了一些函数式编程工具,比如等下要用到的装饰器;operator包含了很多预定义的函数,如算术运算add等,高阶函数map等;requests用于发送请求。
接着,我们从agents
模块引入 AgentExecutor
、create_openai_tools_agent
。AgentExecutor
在LangChain
框架中扮演着核心角色,它负责执行和管理不同类型的Agent, 并协调它们之间的交互。create_openai_tools_agent
用于创建一个与OpenAI服务集成的Agent。让我们以Agent的方式调用OpenAI。
紧接着,langchain_core
提供了相应角色message类,这里引入了BaseMessage
和 HumanMessage
。BaseMessage是基类,定义了消息的基本结构和行为,HumanMessage可以用来表示用户的chat。
再者,from langchain.output_parsers.openai_functions import JsonOutputFunctionsParser
输出格式化我们使用了JsonOutputFunctionsParser。LLM返回的是文本,output_parsers
格式器提供了openai_functions
,JsonOutputFunctionsParser函数会将文本转成json格式的输出。
又有 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
langchain_core
的prompts
模块提供了MessagesPlaceholder和ChatPromptTemplate,用于Prompt设计。
接下来, from langgraph.graph import StateGraph, END
LangGraph出场了,它是LangChain
新功能,让我们从graph里引入StateGraph和结束结点
最后 from langchain_openai import ChatOpenAI
在 LangChain 框架中引入一个与 OpenAI 的聊天模型,再后面是一堆的类型维束引入。
- 实例化LLM
ini
llm = ChatOpenAI(model="gpt-4-turbo-preview")
大家也可以使用gpt-3.5-turbo, 但是在处理一些复杂的agent时,gpt-4表现更优异。
- 定义搜索函数
python
from langchain_core.messages import ( AIMessage, BaseMessage, ChatMessage, FunctionMessage, HumanMessage, SystemMessage )
@tool("web_search")
def web_search(query: str) -> str:
"""Search with Google SERP API by a query"""
search = SerpAPIWrapper()
return search.run(query)
Message 的类型真多啊,这个交给下次开篇讲,这里我们focus等下生成agent的web_search函数。functools
提供的装饰器模式,将web_search
装饰成为能够搜索的工具函数。这个函数负责调用SerpAPIWrapper,根据传入的查询参数,拿到google的搜索结果。
- twitter_writer 工具函数
python
@tool("twitter_writer")
def write_tweet(content: str) -> str:
"""Based a piece of content, write a tweet."""
chat = ChatOpenAI()
messages = [
SystemMessage( content="You are a Twitter account operator."
" You are responsible for writing a tweet based on the content given."
" You should follow the Twitter policy and make sure each tweet has no more than 140 characters." ),
HumanMessage( content=content ), ]
response = chat(messages)
return response.content
装饰器仍然是帮助函数成为创建agent的工具函数。函数通过ChatOpenAI提供的聊天实例,然后准备好消息数组。SystemMessage 是聊天开始前的系统设置,在这里指定了它的角色是Twitter writter, 任务是基于给定任务写一篇tweet, 要求不超过140字。HummanMessage 即用户聊天信息,content是传进来的内容,接着 response = chat(messages) 让聊天模型运行, 并返回结果。这个工具函数主要负责生成。从这里可以看出,twitter的生成是以聊天模型调用的方式生成。
- GraphState
python
class AgentState(TypedDict):
# The annotation tells the graph that new messages will always
# be added to the current states
messages: Annotated[Sequence[BaseMessage], operator.add]
# The 'next' field indicates where to route to next
next: str
langgraph
的第一个核心是graphstate, 这个状态标记当前的应用状态。 typing
是 Python 的一个标准库模块,用于提供Type Hints功能。AgentState是一个基于TypedDict的类,里面包含相应的字段。
- 创建一个agent
css
def create_agent(llm: ChatOpenAI, tools: list, system_prompt: str):
prompt = ChatPromptTemplate.from_messages( [ ( "system", system_prompt, ),
MessagesPlaceholder(variable_name="messages"), MessagesPlaceholder(variable_name="agent_scratchpad"), ] )
agent = create_openai_tools_agent(llm, tools, prompt) executor = AgentExecutor(agent=agent, tools=tools)
return executor
def agent_node(state, agent, name):
result = agent.invoke(state) return {"messages": [HumanMessage(content=result["output"], name=name)]}
封装了一个create_agent方法,参数为llm、tools、system_prompt。我们这里要做的是一个聊天agent。我们准备了prompt, 调用ChatPromptTemplate
的from_messages方法。调用create_openai_tools_agent方法生成聊天agent,接受了参数分别是llm,tools数组, prompt。返回AgentExectutor的实例。
- supervisor_chain
ini
# 两个agent
members = ["Search_Engine", "Twitter_Writer"]
makefile
system_prompt = (
"You are a supervisor tasked with managing a conversation between the"
" following workers: {members}. Given the following user request,"
" respond with the worker to act next. Each worker will perform a"
" task and respond with their results and status. When finished,"
" respond with FINISH." )
在这个系统prompt里,我们首先指定它是一个监管上面两个agent的supervisor代理。
ini
options = ["FINISH"] + members
ini
function_def = { "name": "route", "description": "Select the next role.", "parameters": { "title": "routeSchema", "type": "object", "properties": { "next": { "title": "Next", "anyOf": [ {"enum": options}, ], } }, "required": ["next"], }, }
less
prompt = ChatPromptTemplate.from_messages( [ ("system", system_prompt), MessagesPlaceholder(variable_name="messages"), ( "system", "Given the conversation above, who should act next?" " Or should we FINISH? Select one of: {options}", ), ] ).partial(options=str(options), members=", ".join(members))
supervisor_chain = ( prompt | llm.bind_functions(functions=[function_def], function_call="route") | JsonOutputFunctionsParser() )
- 生成相应的agent和节点
ini
# 调用create_agent方法,tool 为web_search, prompt 给的身份是web 搜索引擎
search_engine_agent = create_agent(llm, [web_search], "You are a web search engine.")
# 搜索引擎节点
functools.partial会返回一个对象,它预填了agent_node函数的部分参数,agent为 search_engine_agent, 名字为Search_Engine
search_engine_node = functools.partial(agent_node, agent=search_engine_agent, name="Search_Engine")
# twitter 写作agent
twitter_operator_agent = create_agent(llm, [write_tweet], "You are responsible for writing a tweet based on the content given.")
# twitter写作节点
twitter_operator_node = functools.partial(agent_node, agent=twitter_operator_agent, name="Twitter_Writer")
# 实例化StateGraph, 将上面的AgentState 作为状态
workflow = StateGraph(AgentState)
# 给工作流添加工作节点Search_Engine和Twitter_Writer
workflow.add_node("Search_Engine", search_engine_node) workflow.add_node("Twitter_Writer", twitter_operator_node)
# 最后再添加监督节点,它会看在每个工作节点的状态
workflow.add_node("supervisor", supervisor_chain)
- 添加边
css
for member in members:
workflow.add_edge(member, "supervisor")
# 条件边
conditional_map = {k: k for k in members}
# 是否结束
conditional_map["FINISH"] = END
# 给监管节点添加条件边 加上supervisor上,它来决定
# lambda x: x["next"] 去看在节点的next 值 是多少,下一步做什么
workflow.add_conditional_edges("supervisor", lambda x: x["next"], conditional_map)
# 将supervisor 添加为初始点
workflow.set_entry_point("supervisor")
# compile 编译生成graph
graph = workflow.compile()
添加完后如图
- 给任务,并监听状态
bash
# graph.stream是一个流式接口,遍历所有的节点,拿到输出信息
for s in graph.stream(
{
"messages": [
# 使用HumanMesasage 代表用户发表任务
HumanMessage(content="Write a tweet about LangChain news") ]
}
):
# 如果没有__end__ 就输出反馈
if "__end__" not in s:
print(s)
print("----")
总结
静下心来,分析了一版LangChain关于LangGraph
的代码,对Prompt、Agent的理解更深入了。
LangGraph还是不如AutoGen 聊天式多代理好理解,代理间的聊天是用chatOpenAI解决的,代理间的执行顺序是由边和条件边来处理的,而任务状态由grapState来搞定。