Ollama + LangGraph 用户交互
·
Ollama + LangGraph + Agent 用户交互
环境: WSL2 >> Ubuntu 20.04 >> Docker: Docker(Ollama) + Docker(Python 3.12.13)
认知:大模型是过去时
通过调用大模型的API来实现大模型的应用,这种方式拿到的大模型回复不是进行时,而是过去时.我们给大模型法送一条指令,大模型会根据指令进行思考,然后回复.这个回复是大模型已经思考完,执行完的回复,在大模型思考或者执行过程中,我们无法打断它.
比如:我们叫大模型新建一个文本文档,当大模型新建文档时,发现文档已经存在了,他就会停止新建,并且回复我们文档已经存在了,是否需要覆盖.这个时候我们表观上感觉他在和我们进行语音交互,实际上他回复我们后就关闭了思考回到了原始状态,从大模型的角度来说,刚才发生了什么他已经不知道了.我们这个时候再发送:请覆盖文档,其实大模型他是无法理解的.如果对这个认识不到位,我们常常会犯下原则的错误.如:
from langchain.agents import create_agent
from langchain.tools import tool
from langchain.llms import ChatOllama
from langchain.messages import AIMessage, HumanMessage
@tool
def write_file(filename: str, content: str, to_cover:bool = False) -> str:
try:
if not filename.endswith('.txt'):
return f'文件名{filename}不是文本文件'
# 防止文件名包含特殊字符
filename = os.path.basename(filename)
if os.path.exists(filename) and not to_cover:
return f'文件{filename}已存在, 提醒用户确认是否要覆盖?'
else:
with open(filename, 'w', encoding='utf-8') as f:
f.write(content)
return f'文件{filename}已成功写入, 写入内容{len(content)}个字符'
except Exception as e:
return f'写入文件{filename}时出错: {e}'
tools = [write_file]
llm = ChatOllama(model='llama2')
agent = create_agent(llm, tools)
response = agent.invoke(HumanMessage(content='请新建一个文本文档test.txt, 随机写入内容'))
for message in response['messages']:
if isinstance(message, AIMessage):
if '覆盖' in message.content:
agent.invoke(HumanMessage(content='是'))
#....
表面上看没问题,运行过程中也不报错,但就是无法生成文件.因为在大模型思考结束后,你再发送’是’的时候,大模型已经忘记刚才要做什么了,所以他对你发送的’是’就无法理解了.
常规解决方法:每次invoke的时候,都要发送完整的消息,让大模型从头理解会话记录,然后执行命令.
# ...
tools = [write_file]
llm = ChatOllama(model='llama2')
agent = create_agent(llm, tools)
history_messages = []
history_messages.append(HumanMessage(content='请新建一个文本文档test.txt, 随机写入内容'))
response = agent.invoke(history_messages)
for message in response['messages']:
if isinstance(message, AIMessage):
history_messages.append(message)
if '覆盖' in message.content:
history_messages.append(HumanMessage(content='是'))
agent.invoke(history_messages)
# ...
这样就可以成功覆盖文件了.但你仔细观察就会发现,这样也时而成功,时而失败.并且你还会发现大模型最开始给你的随机内容,和真实写入文件的随机内容不一样?因为同样的操作,你让大模型执行了两次,结果是两次的随机内容不一样.这样的问题也可以解决,因为我们在遍历messages的时候,忽略了ToolCall的message就是ToolMessage,所以我们把ToolMessage的判断继续加入for循环中.
这只是一个示例,然而在实际操作中,逻辑会复杂的多,那么for里面的内容就会复杂得多.随着逻辑复杂,我们for的主体就会越来越乱,没有固定的结构,维护起来就会越来越困难,到最后甚至维护不了,直接重写.
LangGraph 解决前面的问题
这里不是教程,而是解决前面的问题.所以LangGraph是什么,就烦请客官自寻官方文档.
"""
langgraph_confirm.py 通过状态图实现智能体 call function 并且与用户进行交互
LangGraph(智能体编排框架)
StateGraph 状态图,通过状态来定义智能体的运行流程.
在智能体运行链中定义节点, 并根据节点的状态进行路由.也可以循环执行节点.
entry_point 启动节点
node 节点
edge 边, 用于连接节点. 可以根据节点的状态进行路由. 也可以循环执行节点.
conditional_edges 条件边, 用于根据节点的状态进行路由.
"""
import uuid, os, pprint
from langchain.messages import HumanMessage, AIMessage, SystemMessage, ToolMessage
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages, BaseMessage
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from typing import Annotated, TypedDict, Literal
from langchain_ollama import ChatOllama
# ==============================================================
# 状态定义
# 状态会在节点中传递, 用于存储节点的状态.所以历史会话也保留在状态中.
# 运行id 用于标识运行,避免不同运行之间的状态冲突.
# ==============================================================
class State(TypedDict):
messages: Annotated[list[BaseMessage], add_messages] # Annotated定义赋值/更新规则:[列表,追加] 一条一条累加的messages BaseMessage是langchain的消息基类
need_confirm:bool # 是否需要用户确认
confirm_text:str # 确认文本
run_id:str # 运行id
# ==============================================================
# 工具定义 Tools
# ==============================================================
def write_file(filename:str, content:str, to_cover:bool = False) -> str:
"""
写入文件
参数: filename 文件名, content 内容, to_cover 是否覆盖, 默认False,需要用户确认后方可改为True
返回: 写入结果
"""
try:
mode = "w" if to_cover else "x"
with open(filename, mode, encoding="utf-8") as f:
f.write(content)
return f"文件{filename}已创建"
except FileExistsError as e:
return f'文件{filename}已存在, 需要用户确认是否要覆盖?(y/n)'
except Exception as e:
return f'文件{filename}创建失败: {e}'
def directory_list(directory:str = ".") -> str:
try:
files = os.listdir(directory)
return "\n".join(files)
except Exception as e:
return f'目录{directory}不存在: {e}'
def random_txt(length:int = 10)->str:
"""
随机生成中文字符串
参数: length 字数, 默认10
返回: 随机生成的中文字符串
"""
res = []
for _ in range(length):
# 随机中文编码
code = random.randint(0x4e00, 0x9fa5)
res.append(chr(code))
return ''.join(res)
# ==============================================================
# 模型定义
# bind_tools 绑定工具, 这种方式绑定工具,工具的优先级比 langchain中create_agent 绑定工具优先级高, 所以提示词一定要着重强调工具的调用.
# prompt | llm 将提示词绑定到模型中, 并返回一个模型对象(调用模型对象的invoke方法, 可以运行模型),最后将这个对象加入到状态图中, 用于在节点中调用模型
# ==============================================================
prompt = ChatPromptTemplate.from_messages([
("system","""你是一名文明的文件助手,你要按照以下规则进行操作:
1: 你必须使用中文描述你要进行的操作
2: 当用户要求写文件时,你必须同时**完成两件事**:
- 在content中说明你将要执行的操作请请求确认
- 在tool_calls中附上对应的工具调用
3: 你必须严格调用工具,不能直接执行代码
4: 操作文件前必须得到用户的确认,确认用语类似:我现在将创建文件,您是否确认继续操作(y/n)?
5: 用户输入回答y或者确认或者继续表示用户确认,你必须继续执行操作
6: 没有用户的确认,你必须拒绝执行函数
"""),
MessagesPlaceholder("messages"), # 占位符,添加历史消息 模型接受消息一般是: system, messages
])
tools = [write_file, directory_list, random_txt]
llm = ChatOllama(
model="qwen2.5:7b",
temperature=0.2,
base_url="http://host.docker.internal:11434",
).bind_tools(tools)
tool_llm = prompt | llm # 绑定prompt和模型 tool_ll就是运行模型的接口(模型对象)
def call_model(state:State):
messages = state['messages']
print(f'{'*'*50} messages:')
pprint.pprint(messages)
response = tool_llm.invoke(state)
print(f'{'*'*50} response消息:')
pprint.pprint(response)
return {
'messages': [response],
'need_confirm': any(key in response.content for key in ['确认', 'y/n', '继续', 'y', '吗?']),
}
def execute_tool(state:State):
message = state['messages'][-1]
calls = message.tool_calls
results = []
for call in calls:
fun = {f.__name__: f for f in tools}[call['name']]
results.append(ToolMessage(content=fun(**call['args']), tool_call_id=call['id']))
return {
'messages': results,
'need_confirm': False,
}
def human_confirm(state:State):
message = state['messages'][-1]
human_input = input(f'[AI ASK]: {message.content}')
return {
'messages': [HumanMessage(content=human_input.strip())],
'need_confirm': False,
'confirm_text': human_input.strip(),
'run_id': message.id,
}
def judge_node(state:State) -> Literal["human_confirm", "execute_tool", "end"]:
message = state['messages'][-1]
print(f'{'*'*50} need_confirm:{state['need_confirm']}')
if state['need_confirm']:
return 'human_confirm'
if hasattr(message, 'tool_calls') and message.tool_calls:
return 'execute_tool'
return 'end'
# ==============================================================
# 通过状态图(StateGraph)创建代理(agent)
# ==============================================================
def create_confirm_agent():
# 实例化状态图
graph = StateGraph(State)
# 添加节点
graph.add_node('call_model', call_model)
graph.add_node('execute_tool', execute_tool)
graph.add_node('human_confirm', human_confirm)
# graph.add_node('judge_node', judge_node)
# 入口
graph.set_entry_point('call_model')
# 添加条件边界(可以有多个条件边界)
graph.add_conditional_edges('call_model', judge_node, {'human_confirm': 'human_confirm', 'execute_tool': 'execute_tool', 'end': END})
# 添加边
graph.add_edge('human_confirm', 'call_model')
graph.add_edge('execute_tool', 'call_model')
return graph.compile() # 编译状态图, 生成智能体.(agent)
if __name__ == '__main__':
agent = create_confirm_agent()
state = {
"messages":[
HumanMessage(content="请创建一个txt文件, 文件名test.txt, 并且调用工具生成随机中文内容,写入文件"),
],
"need_confirm": False,
"confirm_text": "",
"run_id": str(uuid.uuid4()),
}
print(f'{'*'*50} 测试写入文件功能 {'*'*50}')
apply = agent.invoke(state)
print(f'{'*'*50} 测试结果:')
print('\n')
pprint.pprint(apply)
"""
流程图
Start: 流程开始。
call_model: 调用 LLM 生成响应,判断是否需要确认或调用工具。
judge_node: 根据状态进行路由:
若 need_confirm 为真,进入 human_confirm。
若存在 tool_calls,进入 execute_tool。
否则,进入 END。
human_confirm: 等待用户输入确认信息,完成后返回 call_model。
execute_tool: 执行具体工具函数,完成后返回 call_model。
END: 流程结束
"""
内容优化方向
结构化输出, 例如:json格式,
checkpointer 保存对话历史
将同步invoke改为异步ainvoke
更多推荐


所有评论(0)