LangGraph
- 有限无环图
- LangGraph 不是一个颠覆性的框架,利用 LangGraph 构建的组件仍然是一个 Runnable 可运行组件,所以它是 LCEL 的扩展,不仅可以单独使用,还可以和 LCEL 链应用、原始的 LangChain Runnable 可运行组件、丰富的大量第三方集成、甚至与其他图结构应用进行嵌套结合,从而让 LLM/Agent 应用的开发变得非常简单。
- LangGraph 提供了以下核心优势:循环、可控制性和持久性。LangGraph 允许定义设计循环、条件判断的流程
- 循环和分支
- 持久化
- 人机交互
- 流支持
使用
操作
- 6 个步骤来执行 LangGraph 1.初始化大语言模型和工具(ChatOpenAI、tools)。 2.用状态初始化图架构(StateGraph 状态图)。 3.为图定义每一个节点(add_node 函数为图添加节点)。 4.定义图的起点、终点和节点边(add_edge 函数为图添加边)。 5.编译图架构为 Runnable 可运行组件(graph.compile 函数编译图)。 6.调用编译后的 Runnable 可运行组件执行图(graph.invoke 函数调用图)。
步骤
- 创建 state
- messagesStateReducer 是归纳函数,约定了 state 中的 messages 这个值的更新规则,现在这个是累加,意味着每一个节点返回的 messages 都会累加到 state中;如果没有归纳函数,就是直接替换
js
import { BaseMessage, BaseMessageLike } from '@langchain/core/messages';
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
export const StateAnnotation = Annotation.Root({
messages: Annotation<BaseMessage[], BaseMessageLike[]>({
reducer: messagesStateReducer,
default: () => [],
}),
// additionalField: Annotation<string>,
});
- 创建 graph
- 定义节点 callModel,route,节点也就是一个函数,接受 state 和 runnableConfig,
- route 是条件节点,判断条件走向
- 构建一个图
js
new StateGraph(StateAnnotation)
.addNode('callModel', callModel) // 添加节点
.addNode('customAdd', customAdd)
.addEdge(START, 'callModel') // 添加实体边
.addEdge('callModel', 'customAdd')
.addConditionalEdges('customAdd', route); // 添加条件边
js
/**
* Starter LangGraph.js Template
* Make this code your own!
*/
import { StateGraph, START } from '@langchain/langgraph';
import { RunnableConfig } from '@langchain/core/runnables';
import { StateAnnotation } from './state.js';
const callModel = async (
state: typeof StateAnnotation.State,
_config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
console.log('Current state:', state);
return {
messages: [
{
role: 'assistant',
content: `Hi there! How are you?`,
},
],
};
};
const customAdd = () => {
return {
messages: [
{
role: 'assistant',
content: `my name is youyou`,
},
],
};
};
export const route = (
state: typeof StateAnnotation.State,
): '__end__' | 'callModel' => {
console.log('state.messages :', state.messages.length);
if (state.messages.length > 5) {
return '__end__'; // 最终要回到 end 结束节点上
}
// Loop back
return 'callModel';
};
// Finally, create the graph itself.
const builder = new StateGraph(StateAnnotation)
.addNode('callModel', callModel)
.addNode('customAdd', customAdd)
.addEdge(START, 'callModel')
.addEdge('callModel', 'customAdd')
.addConditionalEdges('customAdd', route);
export const graph = builder.compile(); // 开始编译
graph.name = 'New Agent';
- 直接调用 graph 也是 runnable
js
// 调用
(async function () {
const res = await graph.invoke({ messages: [], userName: 'haha' });
console.log('=>(demo.ts 26) username', res.userName);
res.messages.forEach((m) => {
console.log('=>(graph.ts 72) m', m.content);
});
})();
graph实现工具调用-条件调用边
- 思路
- 将每一次的消息都返回给模型
- route节点,取出最后一条message,判断是否包含工具函数 tool_call 属性(里面有函数名和调用参数)
- 如果有,就把 tool_call 里面的函数名,调用参数取出来,直接调用tool.invoke(因为tool也是runnable),调用的结果包装成 ToolMessage,返回
- 直到最后一次,没有 tool_call的时候,路由到 end 节点
js
/**
* Starter LangGraph.js Template
* Make this code your own!
*/
import { StateGraph, START } from '@langchain/langgraph';
import { RunnableConfig } from '@langchain/core/runnables';
import {
BaseMessage,
AIMessage,
BaseMessageLike,
} from '@langchain/core/messages';
import { Annotation, messagesStateReducer } from '@langchain/langgraph';
import { ChatOpenAI } from '@langchain/openai';
import { ToolMessage, HumanMessage } from '@langchain/core/messages';
import 'dotenv/config';
import { SerpAPI } from '@langchain/community/tools/serpapi';
import { Calculator } from '@langchain/community/tools/calculator';
const serpTool = new SerpAPI(process.env.SERP_KEY);
const calculatorTool = new Calculator();
const tools = [serpTool, calculatorTool];
const llm = new ChatOpenAI({
modelName: 'gpt-3.5-turbo-16k',
configuration: {
baseURL: process.env.OPENAI_API_BASE_URL,
},
});
const llmWithTools = llm.bindTools(tools);
const StateAnnotation = Annotation.Root({
messages: Annotation<AIMessage[]>({
reducer: messagesStateReducer,
default: () => [],
}),
});
const chatbot = async (
state: typeof StateAnnotation.State,
_config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
const ai_messages = await llmWithTools.invoke(state.messages);
return {
messages: [ai_messages],
};
};
const tool_executor = async (
state: typeof StateAnnotation.State,
_config: RunnableConfig,
): Promise<typeof StateAnnotation.Update> => {
// 提取数据状态中的 tool_calls
const tool_calls = state.messages.at(-1).tool_calls;
// 根据找到的 tool_calls去获取需要执行什么工具
const tools_by_name = tools.reduce((acc, tool) => {
acc[tool.name] = tool;
return acc;
}, {});
// 执行工具得到对应的结果
const messages = [];
for (const tool_call of tool_calls) {
const tool = tools_by_name[tool_call.name];
const result = await tool.invoke(tool_call.args);
messages.push(new ToolMessage(result, tool_call.id, tool_call.name));
}
return {
messages: messages,
};
};
export const route = (
state: typeof StateAnnotation.State,
): '__end__' | 'tool_executor' => {
const ai_message = state.messages.at(-1);
// Loop back
if ('tool_calls' in ai_message && ai_message.tool_calls.length > 0) {
return 'tool_executor';
}
return '__end__';
};
// Finally, create the graph itself.
const builder = new StateGraph(StateAnnotation)
.addNode('llm', chatbot)
.addNode('tool_executor', tool_executor)
// 添加边
.addEdge(START, 'llm')
.addConditionalEdges('llm', route)
.addEdge('tool_executor', 'llm');
export const graph = builder.compile();
// 调用 graph
(async function () {
const res = await graph.invoke({
messages: [
new HumanMessage(
'I have 18 US dollars, how much is it equivalent to in RMB?',
),
],
});
res.messages.forEach((m) => {
console.log('=>(graph.ts 72) m', m.content);
});
})();
graph.name = 'tool Agent';
并行调用
- 就是一个节点,addEdge到两个节点上
- 理解一下,langGraph 中的节点都是去修改一个全局的 State,并不是像 chain 将执行的一个结果流到下一个
- 所以当一个节点执行了,但是没有连接到 end 节点,那么全局的 state 也会有他修改的数据
预设的
- import { ToolNode } from '@langchain/langgraph/prebuilt';
- import { createReactAgent } from '@langchain/langgraph/prebuilt';
本质上 createReactAgent 内部都是 工具函数的调用形式了,之前的靠 prompt 的形式渐渐不用了,一是依靠大模型反馈的不稳定,二是现在的大模型都支持工具调用
消息的处理
- 删除消息 - RemoveMessage
- 本质上也是根据 id 来删除的
- 更新消息
- 返回消息的 id 和内容,就是更新
- 修剪消息
- trimMessage - 对一批消息,进行有规则的裁剪,去除
js
const updateMsg = await trimMessages(messages, {
maxTokens: 80,
tokenCounter: llm,
strategy: 'first',
endOn: 'human',
allowPartial: true,
});
- max_tokens:修剪消息的最大 Token 数。
- strategy:修剪策略,first 代表从前往后修剪消息,last 代表从后往前修剪消息,默认为 last。
- token_counter:计算 Token 数的函数,或者传递大语言模型(使用大语言模型的 .get_num_tokens_from_messages() 计算 Token 数)。
- allow_partial:如果只能拆分消息的一部分,是否拆分消息,默认为 False,拆分可以分成多种,一种是消息文本单独拆分,另外一种是如果设置了 n,一次性返回多条消息,针对消息的拆分。
- end_on:修剪消息结束的类型,如果执行,则在这种类型的最后一次出现时将被忽略,类型为列表或者单个值(支持传递消息的类型字符串,例如:system、human、ai、tool 等,亦或者传递消息类)。
- start_on:修剪消息开始的类型,如果执行,则在这种类型的最后一次出现时将被忽略,类型为列表或者单个值(支持传递消息的类型字符串,例如:system、human、ai、tool 等,亦或者传递消息类)。
- include_system:是否保留系统消息,只有在 strategy="last" 时设置才有效。
- text_splitter:文本分割器,默认为空,当设置 allow_partial=True 时才有用,用于对某个消息类型中的大文本进行分割。
检查点与线程 checkpoint
- 在 LangGraph 中,检查点 通常用于记录或标记程序在某个阶段的 状态,以便在程序运行过程中出现问题时,可以回溯到特定的状态,亦或者在图执行的过程中将任意一个节点的状态进行保存
- 使用
- 传入 检查点
js
const checkpointer = new MemorySaver();
const agent = createReactAgent({
llm: model,
tools: [getWeather],
checkpointer, // 这个也类似与普通的 graph.complie({ checkpointer })
});
- 在执行的时候,传入线程id,就可以有记忆功能了
js
const res = await agent.invoke(
{
messages: [
{ role: 'user', content: '我叫晓晓宝,喜欢打篮球,你喜欢什么' },
],
},
{
configurable: { thread_id: 1 },
},
);
console.log('=>(6.checkpoint.ts 48) res', res);
const res1 = await agent.invoke(
{
messages: [{ role: 'user', content: '我叫什么' }],
},
{
configurable: { thread_id: 1 },
},
);
console.log('=>(6.checkpoint.ts 48) res', res1);
断点 interruptAfter interruptBefore
- 断点 建立在 LangGraph 检查点之上,检查点在每个节点执行后保存图的状态,并且 检查点 可以使得图执行可以特定点暂停,等待人为批准,然后从最后一个检查点恢复执行
- invoke 时传入 interruptAfter =[Node] ,会在节点中断
- 判断后,调用 graph.invoke(null,config) 继续图的执行
更改图中的状态
- graph.get_state
- graph.update_state
子图实现类 多Agent
- 例如我们实现一个 营销智能体,其功能为 根据用户传递的原始问题生成一篇【直播带货】脚本,一篇【小红书推广】文案,在这里用户传递一段原始 Prompt,会调用两个 Agent 智能体并行完成各自的任务,最后再进行合并输出
- 判断是使用 LCEL 亦或者 LangGraph 来构建程序的标准如下
- 1.应用是顺序线性,并且无条件分支、无循环,优先考虑 LCEL 表达式;
- 2.存在任意条件分支亦或者任意循环,则该部分可以使用 LangGraph 构建,其余部分仍然可以使用 LCEL 表达式进行拼接;
- 3.在节点组件较多,并且难以提取出 公共数据状态 的情况下,可以优先使用 LCEL 表达式,然后再使用 LangGraph 改造;
- 而无论是构建 LCEL 亦或者是 LangGraph 应用,其步骤都是大差不差:
- 分析使用 LCEL 还是使用 LangGraph 来实现,亦或者是混合使用。
- 确定整个程序的节点和各个边,涵盖了起点、终点、条件边、循环等。
- 提炼各个节点之间的公共数据,制作 数据状态,确定归纳函数逻辑,亦或者使用覆盖更新的方式。
- 完成应用程序的各个节点函数,并构建图,添加节点。
- 按照应用程序的流向为各个节点添加边,从起点开始,直到结束。
- 编译程序,检测是否需要检查点,是否需要断点等功能。
- 调用程序并提取输出内容。
实现(技巧)
- 子图的数据是继承父图,因为子图要更新父图的数据(全局数据)
- 子图也有自己的数据
python
class AgentState(TypedDict):
query: Annotated[str, reduce_str] # 原始问题
live_content: Annotated[str, reduce_str] # 直播文案
xhs_content: Annotated[str, reduce_str] # 小红书文案
class LiveAgentState(AgentState, MessagesState): // MessagesState 这里就是子图的数据,封装好的历史的消息管理
"""直播文案智能体状态"""
pass
class XHSAgentState(AgentState):
"""小红书文案智能体状态"""
pass
- 添加工具变,和工具节点,都有封装好的方法和节点,不用自己写 ToolNode,tools_condition
js
live_agent_graph.add_node("tools", ToolNode([google_serper]))
live_agent_graph.add_conditional_edges("chatbot_live", tools_condition)
- 生成文案的节点,除了要更新全局的数据,也要更新自己的数据
- 如果节点,每次 invoke 的值并不是所有的 message,记得要把历史消息也记录并且传递(placeholder),不然丢失了上下文,可能一直在使用工具搜索生成
python
def chatbot_live(state: LiveAgentState, config: RunnableConfig) -> Any:
"""直播文案智能体聊天机器人节点"""
# 1.创建提示模板+链应用
prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个拥有10年经验的直播文案专家,请根据用户提供的产品整理一篇直播带货脚本文案,如果在你的知识库内找不到关于该产品的信息,可以使用搜索工具。"
),
("human", "{query}"),
("placeholder", "{chat_history}"),
])
chain = prompt | llm.bind_tools([google_serper])
# 2.调用链并生成ai消息
ai_message = chain.invoke({"query": state["query"], "chat_history": state["messages"]})
return {
"messages": [ai_message], // 这个就是直播文案自己的 message 私有数据。。。小红书文案也有
"live_content": ai_message.content,
}
图实现 CRAG(纠正性索引增强生成)
- 需求,开始在构建检索器,搜索与问题相关的一些文章。搜索出来的数据会有一个 检索质量评估,
- 如果评估出来是与问题关联的,这放入到 Document 中,用模型输出;
- 如果评估出来现有的文章与问题关联度不高
- 则先用 LLM 优化提问
- 再借用工具去搜索文章,搜索到的再参与质量评估中
实现(技巧)
- 节点 Node
- 评估关联性节点 -> with_structured_output强制使用函数固定输出格式
python
class GradeDocument(BaseModel):
"""文档评分Pydantic模型"""
binary_score: str = Field(description="文档与问题是否关联,请回答yes或者no")
system = """你是一名评估检索到的文档与用户问题相关性的评估员。
如果文档包含与问题相关的关键字或语义,请将其评级为相关。
给出一个是否相关得分为yes或者no,以表明文档是否与问题相关。"""
grade_prompt = ChatPromptTemplate.from_messages([
("system", system),
("human", "检索文档: \n\n{document}\n\n用户问题: {question}"),
])
retrieval_grader = grade_prompt | llm.with_structured_output(GradeDocument)
diff
- 网络搜索问题重写节点
python
rewrite_prompt = ChatPromptTemplate.from_messages([
(
"system",
"你是一个将输入问题转换为优化的更好版本的问题重写器并用于网络搜索。请查看输入并尝试推理潜在的语义意图/含义。"
),
("human", "这里是初始化问题:\n\n{question}\n\n请尝试提出一个改进问题。")
])
question_rewriter = rewrite_prompt | llm.bind(temperature=0) | StrOutputParser()
ps:如果大家有疑惑的地方,可以私信咨询我哦~旨在帮助前端er入门生产级别的AI编程