[Ai Agent] 05 LangChain Agents 实战:从 ReAct 到带记忆的流式智能体

博客配套代码发布于github:05 LangChain 进阶

相关Agent专栏:Ai Agent教学

本系列所有博客均配套Gihub开源代码,开箱即用,仅需配置API_KEY。

如果该Agent教学系列帮到了你,欢迎给我个Star⭐,非常感谢!

知识点Agent 构建 (ReAct & Tool-Calling)|特化 Agent (create_sql_agent)|Agent 与 Memory 融合|高阶技巧 (缓存 & 流式)


前言:

上篇文章我们已经把llm、prompt、chain、memory四大模块啃了下来。但这时的它还有点笨:不会调用工具函数来工作。所以本篇我们就要聚焦于此------为它接入这个手:Agents。

(注:这两个概念务必不要弄混。Agent是ai界通用的对智能体的表达 ;而这里的Agents则是Langchain中的核心模块,代指Function callings,函数调用的思想)

除了主要讲述的Agents外,本篇还会加入一些Langchain的高阶技巧,进一步提升agent的灵活性。

一、ReAct 循环与@tool

其实在我们之前的文章[Ai Agent] 03 Function Calling实战:让AI真正"调用工具"完成任务中,我们就已经手动造过轮子 了,知道其原理与过程。Langchain的Agents模块的思路大体也是一致的,只是使用方法变更了下。

在开始学习Agents之前,我们需要先了解下俩Agents的关键零件:

1. 零件1:ReAct循环------agent的大脑

ReAct=Reason(推理/思考)+Action(行动)

这就是Agent内部思考的逻辑循环:

思考:LLM分析任务(我要查看天气)

行动:调用find_weather函数

观察:Langchain自动调用该工具函数并返回数据

思考:LLM观察到结果,并思考(我已经拿到数据,可以回答用户了)

行动:回复用户今天天气。

如果你想真正看到这个流程,推荐输出时,带上verbose=True,来看看ai内部的思考过程。

2. 零件二:工具------@tool

Agent的手和脚,用以定义Agent能做什么。

Langchain为我们封装了一个@tool装饰器,可以一键将任意Python函数封装为Agent可用的工具

我们接下来用一段自定义代码感受下:

python 复制代码
from langchain_core.tools import tool

@tool
def get_weather(location):
    """模拟获得天气信息"""
    return f"{location}当前天气:23℃,晴,风力2级"

@tool
def get_user_name(user):
    """模拟获得用户名字"""
    return f'用户名字是:{user}'


# 封装好我们要用的工具
tools = [get_weather,get_user_name]
print('工具箱已封装完毕!')

这里有些关键点要注意一下:

当用@tool封装某个函数作为工具时,必须要用"""xx"""来为其添加描述信息

这段描述信息可不是注释,而是这个工具的 "提示词" 。如果你不为它添加该提示词就为直接报错。

添加完后,llm就会根据该提示词来判断是否应该调用这个工具。

成功打印后,我们接下来让llm真正调用这个工具。

二、通用Agent

当我们构建一个涵盖Agents(工具调用)模块的智能体时,我们就必须借助一些新的创建agent的函数了。create_xx_agent有几个不同的函数,这里我们只拿出两个最为常用的来讲。

create_tool_calling_agent:它返回结构化的tool_calls请求,最稳定并且准确。

组成顺序:

  1. LLM 2. Prompt 3. Tools(工具列表)
  2. create_tool_calling_agent:把上面三个组装成agent(大脑)
  3. AgentExecutor:Agent的执行器(身体),负责真正运行ReAct循环。

接着进行代码实操:

LLM配置照常,Prompt配置这里要注意下:

ini 复制代码
# prompt配置
prompt = ChatPromptTemplate.from_messages([
    ("system","你是一个聪明的智能助手。当你遇到解决不了的问题时,会调用工具来解决问题。"),
    ("human","{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad") # 必加,Agent的思考过程
])

当使用create_tool_calling_agent时,必须加上这行MessagePlaceholder(varibale_name="agent_scratchpad"),它代表一个临时记事本 ,会记录 整个ReAct流程(思考-动作-观察),并将整个思考过程再让llm看到,从而做出下步决策。

tools配置也跟之前差不多,这里重点要记一下agent与agent_executor的创建流程:

python 复制代码
# tool配置
@tool
def get_weather(location):
    """模拟获得天气信息"""
    return f"{location}当前天气:23℃,晴,风力2级"

@tool
def get_user_name(user):
    """模拟获得用户名字"""
    return f'用户名字是:{user}'


tools = [get_weather,get_user_name]

# 创建Agent(大脑)
agent = create_tool_calling_agent(llm=llm,prompt=prompt,tools=tools)
# 创建AgentExecutor(执行器)--负责运行ReAct循环
agent_executor = AgentExecutor(agent=agent,tools=tools,verbose=True) # 开启verbose以看到ai思考链
# 运行
response = agent_executor.invoke({
    'input':"今天北京的天气怎么样?"
})

print(response)
print()
print(response['output'])

此处verbose=True即代表输出思考链

整个输出流程就是:

llm -- prompt -- tools -- create_tool_calling_agent -- AgentExecutor

哎,你看这个agent = create_tool_calling_agent(llm=llm,prompt=prompt,tools=tools)

有没有感觉它很像当初的chain=llm|prompt|parser?但为什么这里甚至都没有用到chain?

因为这个重要模块已经被封装进create_tool_calling_agent里面了。

这个函数将Chain作为内部推理的核心环节,并在此基础上构建了一个支持记忆、工具调用和循环决策的智能体框架

之前因为我们还不需要用到这个函数来使用Tools,所以当涉及Chain这个概念时还需要造下轮子,学下它怎么使用。

所以之后虽然我们不会再显式创建Chain,但实际上我们仍在沿用这种思想,这点可别忘掉。

输出结果:

输出成功。

这种通用Agent我们了解了,可以自由根据场景选择来自己DIY工具调用 ,是一个可扩展Agent框架。它足以应付我们绝大场景下的工具使用。但也有些场景用它来,得干很多脏活累活。这点Langchain也想到了,接下来介绍另一个特化Agent

三、SQL专用Agent

领域专用 Agent 的封装要求极高:需场景广泛、接口标准、结构清晰。正因如此,真正合适的案例极少。

SQL 查询正是这样的完美范例

它高频、规范、结构明确,却重复繁琐。如果每次都手动构建工具链,效率将非常差。

create_sql_agent 正是为此而生------开箱即用,一键实现自然语言查数据库,大幅简化开发流程。

  • 你不需要手动定义tools,只用给它一个db连接
  • 它会自动"反思":先查表结构,再编写sql,执行sql,再根据结果回答
  • 它的Prompt已经高度优化,能处理复杂的SQL逻辑。

代码编写如下:

ini 复制代码
# 连接数据库 -- LangChain 使用 SQLAlchemy URI (连接方式)
db_uri = f'sqlite:///{db_file}'
db = SQLDatabase.from_uri(db_uri)

# 创建sqlAgent -- 一键完成,无需定义tools,仅告诉它使用openai-tools,即Tool Calling(工具调用)模式
agent_executor = create_sql_agent(
    llm=llm,
    db=db,
    agent_type="openai-tools",
    verbose=True
)

# 运行
response = agent_executor.invoke({"input":"告诉我Alice多大了?"})
print(response['output'])

可以看出,我们仅配置了llm与db,其他啥都没干,就做好了sql_agent。

再来看看执行结果:

AI先是思考:我要查sql,再去表里编写对应sql语句。获得对应数据后,就会将查询结果输出。

四、融合工具与记忆

我们这篇文章05的Agent很智能,之前文章04的Chain有记忆。现在我们把二者合二为一,让它变得更高级。

就拿本模块的第二章的通用Agent为例,我们接着为其加入记忆功能。

ini 复制代码
# 配置prompt(新增俩占位符 一个为对话历史记录,一个为agent的思考过程)
prompt = ChatPromptTemplate.from_messages([
    ('system','你是小智,一个帮助他人的智能助手。当你无法解答当前问题时,会调用工具来解决问题。'),
    MessagesPlaceholder(variable_name="history"),
    ('human','{input}'),
    MessagesPlaceholder(variable_name="agent_scratchpad")
])

prompt这里就是俩占位符都需要,分别作为记忆工具查询使用

python 复制代码
# 配置tool
@tool
def get_weather(location):
    """模拟获得天气信息"""
    return f"{location}当前天气:23℃,晴,风力2级"


tools = [get_weather]
# 配置agent
agent = create_tool_calling_agent(llm=llm,prompt=prompt,tools=tools)
# 配置AgentExecutor
agent_executor = AgentExecutor(agent=agent,tools=tools) # 这里没加verbose=True,想打印日志看思考链的可以自行打印

tool配置如常。

到最终memory的位置,就由它来包裹所有其他的部件,当做一个长期存在的状态

ini 复制代码
# 记忆存储--包装agent_executor
store = {}
def get_session_history(session_id:str):
    if session_id not in store:
        store[session_id] = ChatMessageHistory()
    return store[session_id]


agent_with_memory = RunnableWithMessageHistory(
    runnable=agent_executor,
    get_session_history=get_session_history,
    input_messages_key="input",
    history_messages_key="history"
)
ini 复制代码
# 打印测试
session_id = 'user123'
if __name__ == '__main__':
    while 1:
        user_input = input('\n你:')
        if user_input == 'quit':
            print('拜拜~')
            break
        response = agent_with_memory.invoke(
            {'input': user_input},
            config={'configurable':{'session_id':session_id}}
        )
        print(f"AI:{response['output']}")

测试结果:

很明显,此时的agent已经可以工具调用与持续记忆。

至此我们的Agents(工具调用代理)的知识点已基本掌握完毕。

五、高阶技巧:缓存&流式

我们所做的agent现在看起来已经非常不错了,但真正放到生产端还有俩致命问题:

其一、贵: 每次运行invoke都在烧钱;

其二、慢: 用户每次运行都必须等整个ReAct跑完才能得到想要的答案。

下面我们依次来解决这俩问题:

1. 缓存

想象这么个场景:

你写好了一个比较复杂的项目Agent,有七八个重要组件,你需要确定它们都能成功运行。于是你开始调试,调试了二三十次,确定满意没有问题了。接着一看llm的token调用:也花了二三十次的token钱...

所以我们就要引入缓存 这个概念:简单讲就是跳过llm调用 这一环节,优先保证其他所有模块测试没啥问题。等其他都完毕后再去掉缓存测试一遍llm调用,这样只用花费一次token调用的钱。

所以在开发测试时强烈推荐加入该功能。但真正在生产端环境必须去掉:毕竟它只是回答结果复用,没有任何智能。

使用方法也简单:

设置全局缓存

set_llm_cache(InMemoryCache())

scss 复制代码
# 设置全局缓存
set_llm_cache(InMemoryCache())

# 第一次调用llm(会远程请求)
query = "用中文写一句关于猫的五言诗。"
start_time = time.time()
response1 = llm.invoke(query).content
print(f"第一次调用结果: {response1}")
print(f"第一次运行时间: {time.time() - start_time:.4f} 秒")
print('')

# 第二次调用llm(会命中缓存)
start_time = time.time()
response2 = llm.invoke(query).content
print(f"第二次调用结果: {response2}")
print(f"第二次运行时间 (已缓存): {time.time() - start_time:.4f} 秒")

# 清理
set_llm_cache(None) # 关闭缓存,以免影响后续实例
print('缓存清理完成')

如上,当缓存命中时几乎不费时,而且也不会调用token花钱。

但如果文件项目比较大时,这样一行的内存缓存还是有点不够用,推荐上个文件或者redis缓存。

2.流式

回想一下,当你在网页端用ai工具时,是否它们输出都是一点点排列给你看的?这就是流式输出,像流水一样。

而我们目前的输出必须得全部跑完才能返回看到答案,这明显不是我们想要的,接下来我们就为其加入流式输出:

ini 复制代码
# 配置llm
llm = ChatOpenAI(
    model="deepseek-chat",
    api_key=api_key,
    base_url="https://api.deepseek.com",
    streaming=True,
    callbacks=[StreamingStdOutCallbackHandler()]
)

这里llm注意下llm的配置:

streaming=True: 即允许流式输出;

callback=[StreamingStdOutCallbackHandler()]: 一个回调处理器,当LLM每生成一个token时,自动将其打印到终端上。

其他代码不用动,最后循环时稍微改一下:

ini 复制代码
if __name__ == '__main__':
    while 1:
        user_input = input('\n你:')
        if user_input == 'quit':
            print('拜拜~')
            break
        # 用于标记"AI:"这个内容
        # flush=True保证"AI:"立即输出,而不是等缓存区存满再输出
        print("AI: ", end="", flush=True)
        response = agent_with_memory.invoke(
            {'input': user_input},
            config={'configurable': {'session_id': session_id}}
        )
        print()

这里已经不需要print了,因为我们之前搞callbacks已经做到生成token时自动打印到终端上。再print也只是重复输出。

同时之前我们的AI: 就需要单独列出来写一下,不能直接跟response一块输出了。

如图,流式输出成功。

六、总结

知识点概括Agent 构建 (ReAct & Tool-Calling)|特化 Agent (create_sql_agent)|Agent 与 Memory 融合|高阶技巧 (缓存 & 流式)

如果你能看到这里,那你真的很厉害了!

至此你已完成了整个Langchain 的学习。现有的工具已经完全足够你做90%+ 的智能体。

强烈建议在完成上述Langchain学习后,利用这些模块,自己试着做一个agent。难度也不高,把五大模块(llm,prompt,chain,memory,agnets)几个结合起来即可。

那么接下来就是另一个重头戏了:RAG

这玩意儿也是个狠角。当我们想接入数据库 ,真正超大文本量 ,正儿八经的企业级项目的时候,就必须引入这个模块的思想来做。比如企业自己的agent,要对接某些功能或数据库或本地文档时。

它的知识点与实战同样繁琐且复杂,但笔者仍会将其庖丁解牛式将其细分拆开,确保讲解透彻到位。收拾收拾,下篇进RAG接着干!

相关推荐
绝无仅有7 小时前
某游戏大厂Java面试深度解析:从多线程到JVM调优(二)
后端·面试·github
绝无仅有7 小时前
某游戏大厂Java面试指南:Spring、集合与语言特性深度解析 (三)
后端·面试·github
dwedwswd17 小时前
技术速递|从 0 到 1:用 Playwright MCP 搭配 GitHub Copilot 搭建 Web 应用调试环境
前端·github·copilot
2501_9387738717 小时前
技术速递|解锁 Playwright MCP 高级调试:GitHub Copilot 辅助生成动态元素定位脚本
github·copilot
zhangbaolin18 小时前
基于pypdf和chromadb构建向量库
langchain·chroma·分块·向量库·文件加载
fitpolo19 小时前
Sourcetree图文使用教程(保姆级)
github
算法小菜鸟成长心得20 小时前
如何将本地项目上传至github
github
初学者_xuan20 小时前
零基础新手小白快速了解掌握服务集群与自动化运维(十六)集群部署模块——Keepalived双机热备
运维·自动化·github
逛逛GitHub1 天前
推荐 11 个本周 yyds 的 GitHub 开源项目。
github