1. 从“会思考”到“会动手”:重新认识LangChain Agents

如果你之前跟着我的系列文章一路学过来,现在你的LangChain智能体应该已经具备了不错的“大脑”(LLM)和“短期记忆”(Memory)。它能理解你的指令,也能记住你们刚才聊了什么。但说实话,它还是个“理论派”,只会动嘴皮子。比如你问它“今天北京天气怎么样?”,它能给你分析一通“用户想查询天气”,但也就到此为止了,因为它没有“手”去真正打开一个天气查询的API。这就是我们之前构建的Chain的局限所在。

Agents(智能体),就是来给这个大脑装上“手”和“脚”的。它的核心思想,就是让LLM学会在需要的时候,主动去调用我们预先定义好的工具函数(比如查询天气、搜索数据库、发送邮件),并根据工具返回的结果,再决定下一步是继续调用工具还是直接回答用户。这个过程,就是我们常说的 ReAct(Reasoning + Acting)循环。你可以把它想象成一个经验丰富的侦探:接到案子(用户输入)后,先推理(Reason)需要哪些线索,然后行动(Act)去调查,观察(Observe)调查结果,再基于新线索进行下一轮推理,直到破案。

在LangChain的语境里,Agents这个模块,就是专门用来实现这套“函数调用”逻辑的。别把它和广义的“AI智能体”概念搞混了,在这里,它特指一套让LLM调度和使用工具的框架。我刚开始用的时候也老弄混,后来想了个笨办法:一看到大写的Agents,就默念“工具调用、工具调用”。

那么,一个能用于生产环境的智能体,光会调用工具就够了吗?远远不够。想象一下你要做一个客服助手,它需要能处理多轮对话(记忆),回复不能等半天才一股脑出来(需要流式响应),而且在开发调试阶段反复测试同一个问题成本太高(需要缓存)。这就是我们今天要攻克的工程化难题:将一个基础的、只会ReAct的工具调用Agent,升级为一个集成了流式记忆体、具备完整工程化缓存策略的、响应迅速的生产级系统。

2. 工程化基石:深入ReAct循环与工具定义

在开始搭建高楼之前,我们必须把地基打牢。这个地基,就是ReAct循环和工具(Tool)的精确定义。很多朋友照着教程跑通了代码,但一遇到复杂场景就抓瞎,根本原因就是对这两个“零件”的理解只停留在表面。

2.1 透视ReAct:不只是“思考-行动”

我们都说ReAct是“推理+行动”,但在LangChain的AgentExecutor内部,这其实是一个严谨的状态机。我习惯在开发时永远加上verbose=True参数,就像给Agent装了个“思维可视化”的仪表盘。我们来实操看看:

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool

# 1. 定义工具 - Agent的手
@tool
def search_web(query: str):
    """根据用户输入的问题,使用搜索引擎进行查询并返回摘要。适用于查找实时信息、新闻或未知领域知识。"""
    # 这里模拟一个搜索API的调用
    return f"关于'{query}'的搜索结果:这是一个模拟的搜索结果摘要。"

@tool
def calculate_bmi(weight_kg: float, height_m: float):
    """计算身体质量指数(BMI)。需要提供体重(公斤)和身高(米)。"""
    bmi = weight_kg / (height_m ** 2)
    return f"您的BMI指数为:{bmi:.1f}。健康范围通常在18.5到24.9之间。"

tools = [search_web, calculate_bmi]

# 2. 配置LLM和Prompt
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个严谨的助手。请逐步思考,必要时使用工具。"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad")  # 关键!ReAct的草稿纸
])

# 3. 创建Agent和执行器,并开启“思维可视化”
agent = create_tool_calling_agent(llm=llm, prompt=prompt, tools=tools)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=True)  # 注意这里的verbose=True

# 4. 运行并观察
response = agent_executor.invoke({"input": "姚明的BMI是多少?他身高2.26米,假设体重140公斤。"})
print("\n最终答案:", response['output'])

运行这段代码,你会在控制台看到类似下面的详细日志(我做了精简和注释):

> 进入新的Agent执行链...
思考:用户想知道姚明的BMI。我需要计算BMI,这需要体重和身高。用户已经提供了:身高2.26米,体重140公斤。我应该使用calculate_bmi工具。
行动:调用 `calculate_bmi` 工具,参数:`weight_kg=140`, `height_m=2.26`
观察:工具返回:您的BMI指数为:27.4。健康范围通常在18.5到24.9之间。
思考:我已经得到了计算结果27.4。根据工具返回的信息,健康范围是18.5-24.9,27.4略高于此范围。我可以直接把这个信息告诉用户。
行动:最终回复用户。
最终答案:根据计算,姚明的BMI指数约为27.4。通常成年人的健康BMI范围在18.5到24.9之间,因此这个数值略高于健康范围的上限。

看到了吗?agent_scratchpad这个占位符就像它的草稿纸,LLM会把“思考”、“行动”、“观察”都写在这张纸上,然后基于完整的草稿内容决定下一步。这就是ReAct循环在代码层面的具象化。很多人在自定义Prompt时忘了加这个MessagesPlaceholder,导致Agent完全无法工作,根本原因就是没理解这个“草稿纸”是循环得以持续的关键。

2.2 工具(Tool)定义的艺术:描述即指令

@tool装饰器定义一个函数很简单,但让LLM能准确调用它,却是一门学问。核心就在于函数的文档字符串(Docstring)。这个字符串不是给人看的注释,而是给LLM看的工具使用说明书

我踩过的一个坑是描述得太模糊。早期我写过一个工具叫fetch_data,描述是“获取数据”。结果LLM在任何需要信息的时候都会调用它,完全不受控制。后来我把它改成了“根据用户提供的唯一订单ID,从内部数据库查询订单状态、金额和创建时间。仅当用户明确提及订单号时使用。” 这样一来,精准度立刻大幅提升。

工具定义的最佳实践:

  1. 描述清晰具体:说明工具做什么、输入是什么、输出是什么、在什么场景下使用。
  2. 参数类型提示:务必使用Python类型提示(如query: str, user_id: int)。这能帮助LangChain生成更规范的Schema,让LLM更好地理解。
  3. 处理复杂逻辑:工具函数内部应该处理错误和边缘情况。比如网络请求失败时,是返回一个错误信息字符串,还是抛出异常让Agent重试?这需要根据你的业务逻辑来定。
@tool
def book_meeting_room(room_id: str, start_time: str, duration_minutes: int):
    """
    预订公司会议室。
    参数:
        room_id: 会议室编号,例如 'A-101', 'B-203'。
        start_time: 会议开始时间,格式必须为 'YYYY-MM-DD HH:MM',例如 '2023-10-27 14:30'。
        duration_minutes: 会议持续时间,以分钟为单位,最长不超过240分钟。
    返回:
        预订成功或失败的消息。如果时间冲突或参数无效,会明确说明原因。
    注意:该工具仅用于预订,无法查询会议室空闲状态。
    """
    # 模拟预订逻辑
    if duration_minutes > 240:
        return "失败:单次会议时长不能超过4小时(240分钟)。"
    # ... 调用真正的预订API ...
    return f"成功预订会议室 {room_id},时间:{start_time},时长:{duration_minutes}分钟。"

这样定义后,LLM就能非常清楚地知道:什么情况下该调用这个工具(用户想订会议室),需要向我索要哪些参数(房间号、时间、时长),以及可能得到什么结果。

3. 构建健壮的生产级智能体

掌握了基础零件,我们就可以开始组装一个更健壮、更像真实产品的智能体了。一个“通用Agent”框架是我们的核心车间。

3.1 使用 create_tool_calling_agent 构建核心

create_tool_calling_agent 是目前LangChain中最稳定、最推荐的方式。它专为现代LLM的“函数调用”能力优化,返回结构化的工具调用请求。它的工作流程可以概括为:LLM + Prompt + Tools -> 智能体大脑 -> AgentExecutor(执行器)

这里有个关键点:create_tool_calling_agent 内部已经封装了Chain的逻辑。还记得我们之前学的 prompt | llm | output_parser 这条链吗?在这个函数里,它被内化成了Agent推理循环的一部分。所以,我们不再需要显式地创建Chain,但“链式编排”的思想依然在起作用。

让我们构建一个更复杂的例子,一个集成了多个工具的旅行助手:

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from datetime import datetime

# 定义一系列工具
@tool
def search_flights(departure: str, arrival: str, date: str):
    """查询指定日期、出发地和目的地的航班信息。日期格式为YYYY-MM-DD。"""
    # 模拟航班查询
    return f"找到从{departure}到{arrival}在{date}的航班:CA123(08:00-10:30),MU456(14:20-16:50)。"

@tool
def get_weather_forecast(city: str, date: str):
    """查询指定城市在特定日期的天气预报。日期格式为YYYY-MM-DD。"""
    return f"{city}在{date}的天气预报:晴,气温15-25℃,微风。"

@tool
def convert_currency(amount: float, from_curr: str, to_curr: str):
    """货币转换。支持常见货币如USD, CNY, EUR, JPY。"""
    rates = {"USD_CNY": 7.2, "EUR_CNY": 7.8, "JPY_CNY": 0.047}
    key = f"{from_curr}_{to_curr}"
    if key in rates:
        result = amount * rates[key]
        return f"{amount} {from_curr} = {result:.2f} {to_curr}(汇率仅供参考)。"
    return f"暂不支持{from_curr}到{to_curr}的转换。"

tools = [search_flights, get_weather_forecast, convert_currency]

# 构建智能体
llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
prompt = ChatPromptTemplate.from_messages([
    ("system", "你是一个专业的旅行助手。请根据用户需求,灵活调用工具来获取信息。回答要详尽友好。"),
    MessagesPlaceholder(variable_name="chat_history"),  # 为记忆预留位置
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_tool_calling_agent(llm=llm, prompt=prompt, tools=tools)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False)  # 生产环境可关闭verbose

# 测试一个复杂查询
question = "我计划下周五从北京飞上海,帮我查一下航班,顺便看看那天上海的天气。另外,100美元大概能换多少人民币?"
response = agent_executor.invoke({"input": question, "chat_history": []})  # 暂时没有历史
print("旅行助手:", response['output'])

这个智能体已经具备了处理复合请求的能力:它会先理解问题中包含的三个子任务(查航班、查天气、换算货币),然后规划执行顺序,依次调用工具,最后整合所有结果给出一个连贯的回答。

3.2 特化Agent:以SQL Agent为例的高效封装

不是所有场景都需要我们从零开始定义工具。对于一些高度标准化、极其通用的领域,LangChain提供了“开箱即用”的特化Agent。其中最经典的就是 create_sql_agent

如果你需要让用户用自然语言查询数据库,自己手动去写一个“解析问题->生成SQL->执行->解释结果”的工具链,会非常繁琐且容易出错。create_sql_agent 帮你完成了所有这些脏活累活。

from langchain.agents import create_sql_agent
from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from langchain.sql_database import SQLDatabase
from langchain_openai import ChatOpenAI

# 1. 连接数据库(这里使用SQLite示例)
db_path = "your_database.db"
db_uri = f"sqlite:///{db_path}"
db = SQLDatabase.from_uri(db_uri)

# 2. 一键创建SQL Agent
agent_executor = create_sql_agent(
    llm=ChatOpenAI(model="gpt-3.5-turbo", temperature=0),
    db=db,
    agent_type="openai-tools",  # 使用工具调用模式,更稳定
    verbose=True,
    max_iterations=5,  # 防止无限循环
    early_stopping_method="force"  # 在达到最大迭代次数后强制停止
)

# 3. 像对话一样查询
result = agent_executor.invoke({
    "input": "我们公司销售额最高的前三个产品是什么?分别属于哪个类别?"
})
print(result["output"])

这个Agent的强大之处在于它的“反思”能力。它会先查询数据库的表结构,理解每个字段的含义,再根据你的问题编写出准确的SQL语句。如果执行出错,它会尝试分析错误原因并修正SQL。这一切都封装在create_sql_agent这一个函数调用里,极大地提升了开发效率。对于企业内部的数据查询、报表生成等场景,这几乎是效率提升的“核武器”

4. 记忆融合:打造有“连续对话”能力的智能体

一个没有记忆的Agent,就像金鱼一样,只有7秒记忆,每次对话都是全新的开始。这显然不符合生产级应用的要求。我们需要将之前学到的Memory模块与Agent深度融合,打造流式记忆体

4.1 将记忆嵌入Agent工作流

记忆的本质,是将历史对话作为上下文,传递给下一次的LLM调用。对于Agent来说,我们需要在它的“思考草稿纸”(agent_scratchpad)之外,再提供一张“历史记录纸”(chat_history)。

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.memory import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory

# 工具和LLM定义(略,同上例)
# ...

# 关键:Prompt中需要两个占位符
prompt_with_memory = ChatPromptTemplate.from_messages([
    ("system", "你是旅行助手小游。你能记住我们之前的对话。"),
    MessagesPlaceholder(variable_name="chat_history"),  # 记忆在此注入
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),  # ReAct过程在此记录
])

agent = create_tool_calling_agent(llm=llm, prompt=prompt_with_memory, tools=tools)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False)

# 创建记忆存储和管理器
memory_store = {}  # 简单用字典在内存中存储,生产环境可用Redis、数据库

def get_session_history(session_id: str):
    """根据会话ID获取或创建聊天历史"""
    if session_id not in memory_store:
        memory_store[session_id] = ChatMessageHistory()
    return memory_store[session_id]

# 用RunnableWithMessageHistory包装执行器,实现记忆的自动管理
agent_with_memory = RunnableWithMessageHistory(
    runnable=agent_executor,
    get_session_history=get_session_history,
    input_messages_key="input",  # 指定输入消息的键
    history_messages_key="chat_history"  # 指定历史消息在Prompt中的键
)

# 模拟多轮对话
session_id = "user_001"
print("=== 第一轮对话 ===")
response1 = agent_with_memory.invoke(
    {"input": "我想去杭州旅行,有什么推荐吗?"},
    config={"configurable": {"session_id": session_id}}  # 关键:绑定会话
)
print("小游:", response1['output'])

print("\n=== 第二轮对话(依赖记忆) ===")
response2 = agent_with_memory.invoke(
    {"input": "那里的天气怎么样?"},  # 这里的“那里”指代上一轮的“杭州”
    config={"configurable": {"session_id": session_id}}
)
print("小游:", response2['output'])

在这段代码中,RunnableWithMessageHistory 是一个神奇的“包装器”。它自动帮我们做了三件事:

  1. 会话隔离:通过session_id区分不同用户的对话历史。
  2. 历史管理:在每次调用invoke时,自动从get_session_history函数中取出该会话的历史记录,并插入到Prompt的chat_history占位符中。
  3. 状态保存:将本轮对话的输入和输出自动保存回历史记录,供下次使用。

这样,你的Agent就真正拥有了连续对话的能力。用户可以说“帮我查一下那个地方的天气”,而Agent能准确知道“那个地方”指的是上一轮提到的“杭州”。

4.2 记忆体的工程化考量

简单的内存字典(memory_store = {})在开发阶段没问题,但上了生产环境就是灾难。你需要考虑:

  • 持久化:用户关掉网页或App,下次回来对话历史不能丢。你需要接入数据库(如PostgreSQL, MongoDB)或高速缓存(如Redis)。
  • 容量与清理:历史对话不能无限增长,否则会消耗大量Token成本并可能影响LLM性能。需要设计策略,比如只保留最近10轮对话,或者定期清理过旧会话。
  • 记忆摘要:对于超长对话,可以将早期历史总结成一段摘要,而不是传递全部原始消息,以节省Token。

这里给出一个使用Redis作为记忆后端的示例思路:

import redis
import json
from langchain.memory import ChatMessageHistory
from langchain.schema import HumanMessage, AIMessage

class RedisChatMessageHistory(ChatMessageHistory):
    """基于Redis的持久化聊天历史"""
    def __init__(self, session_id, redis_url="redis://localhost:6379", ttl=86400):
        self.session_id = f"chat_history:{session_id}"
        self.redis_client = redis.from_url(redis_url)
        self.ttl = ttl  # 记录存活时间(秒),例如24小时

    @property
    def messages(self):
        # 从Redis加载消息
        data = self.redis_client.get(self.session_id)
        if data:
            messages_data = json.loads(data)
            messages = []
            for msg in messages_data:
                if msg['type'] == 'human':
                    messages.append(HumanMessage(content=msg['content']))
                else:
                    messages.append(AIMessage(content=msg['content']))
            return messages
        return []

    def add_message(self, message):
        # 添加消息并保存到Redis
        current_messages = self.messages
        current_messages.append(message)
        messages_to_save = []
        for msg in current_messages:
            msg_dict = {"type": "human" if isinstance(msg, HumanMessage) else "ai", "content": msg.content}
            messages_to_save.append(msg_dict)
        self.redis_client.setex(self.session_id, self.ttl, json.dumps(messages_to_save))

    def clear(self):
        self.redis_client.delete(self.session_id)

# 在get_session_history函数中使用这个类
def get_session_history(session_id: str):
    return RedisChatMessageHistory(session_id=session_id)

5. 性能优化双刃剑:缓存与流式响应

当你的智能体功能完备后,你会面临两个最现实的工程问题:成本响应速度。频繁调用LLM API太烧钱,用户等待完整响应时间太长体验差。这就需要祭出我们的两大优化利器:缓存和流式响应。

5.1 开发阶段的“省钱神器”:LLM缓存

在开发调试阶段,我们经常需要反复运行同一段代码,测试逻辑是否正确。如果每次调用都真实请求LLM API,那费用将是惊人的。LLM缓存的核心思想是:对于完全相同的输入,直接返回上次的结果,跳过真实的API调用。

LangChain提供了全局缓存机制,使用起来非常简单:

from langchain.globals import set_llm_cache
from langchain.cache import InMemoryCache, SQLiteCache
import time

# 方案一:内存缓存(最简单,进程重启后失效)
set_llm_cache(InMemoryCache())

# 方案二:SQLite文件缓存(推荐用于开发)
set_llm_cache(SQLiteCache(database_path=".langchain_cache.db"))

llm = ChatOpenAI(model="gpt-3.5-turbo")

# 第一次调用,会真实请求API
start = time.time()
response1 = llm.invoke("讲一个关于Python的笑话")
print(f"第一次结果(耗时{time.time()-start:.2f}s): {response1.content[:50]}...")

# 第二次调用完全相同的内容,命中缓存,瞬间返回
start = time.time()
response2 = llm.invoke("讲一个关于Python的笑话")
print(f"第二次结果(耗时{time.time()-start:.2f}s): {response2.content[:50]}...")

# 清理缓存(上线前务必执行!)
set_llm_cache(None)

重要提示:缓存是开发调试的利器,但绝不能在生产环境默认开启。因为缓存会使得相同的用户问题永远得到相同的回答,智能体失去了“智能”和“实时性”。我的建议是,在config中增加一个use_cache的开关,仅在特定场景(如频繁查询的静态知识)下由业务逻辑控制开启。

5.2 提升用户体验的关键:流式输出

想象一下,你问一个问题,屏幕卡住十几秒,然后突然蹦出整段答案,这种体验非常糟糕。流式输出(Streaming)让答案像打字一样逐个词或逐行出现,极大地提升了感知速度和用户体验。这对于需要长时间进行ReAct思考(多次工具调用)的Agent尤为重要。

实现流式输出需要做两件事:

  1. 在LLM初始化时开启streaming=True并配置流式回调处理器。
  2. 在调用Agent时,使用stream而不是invoke方法。
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
from langchain_openai import ChatOpenAI

# 1. 配置支持流式的LLM
streaming_llm = ChatOpenAI(
    model="gpt-3.5-turbo",
    streaming=True,  # 开启流式
    callbacks=[StreamingStdOutCallbackHandler()]  # 将token实时打印到标准输出
)

# 2. 用这个LLM创建Agent(假设其他部分如prompt, tools已定义)
agent = create_tool_calling_agent(llm=streaming_llm, prompt=prompt, tools=tools)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False)

# 3. 流式调用
print("用户:帮我查一下北京明天飞上海的航班,并告诉我上海的天气。")
print("AI:", end="", flush=True)  # 先打印“AI:”,不换行

# 注意:对于复杂的Agent,流式输出的是最终答案的token,
# 而中间“思考-行动-观察”的ReAct过程在verbose=False时默认不流式输出。
for chunk in agent_executor.stream({"input": "帮我查一下北京明天飞上海的航班,并告诉我上海的天气。"}):
    # 从chunk中提取并输出内容
    if "output" in chunk:
        print(chunk["output"], end="", flush=True)
print()  # 最后换行

这里有一个非常重要的细节:当Agent进行流式输出时,你看到的是LLM生成最终回答的流式过程。而Agent内部“思考要调用哪个工具”、“执行工具”、“观察结果”这些ReAct步骤,默认是不会流式展示的。如果你希望连整个思考过程都流式展示,需要自定义更复杂的回调函数,这通常用于调试而非最终用户交互。

将流式与记忆结合,你就能创造出真正自然、连贯的对话体验。用户每发送一条消息,AI的回复就开始逐字显现,并且能基于之前的所有对话历史进行回应。这是构建聊天机器人、智能客服等交互式应用的基础。

6. 实战:组装一个完整的工程化智能体系统

现在,让我们把所有零件组装起来,构建一个可用于真实场景的、具备记忆、流式响应和开发缓存的智能体系统。我们将构建一个“个人健康助手”,它能记录你的健康数据,并基于历史进行分析。

import os
from typing import Dict, Any
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain.memory import ChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain.globals import set_llm_cache
from langchain.cache import SQLiteCache
from datetime import datetime

# ---------- 第一部分:工具定义 ----------
health_data_store = {}  # 模拟数据库,键为用户ID,值为健康记录列表

@tool
def record_health_data(user_id: str, data_type: str, value: float, note: str = ""):
    """记录用户的健康数据。
    参数:
        user_id: 用户唯一标识。
        data_type: 数据类型,如 'weight_kg'(体重-公斤), 'blood_pressure_sys'(收缩压), 'blood_pressure_dia'(舒张压), 'heart_rate'(心率)。
        value: 测量值。
        note: 可选备注。
    """
    if user_id not in health_data_store:
        health_data_store[user_id] = []
    record = {
        "timestamp": datetime.now().isoformat(),
        "type": data_type,
        "value": value,
        "note": note
    }
    health_data_store[user_id].append(record)
    return f"已记录:{data_type} = {value}。备注:{note}"

@tool
def get_health_trend(user_id: str, data_type: str, days: int = 7):
    """获取用户近期某项健康数据的趋势。
    返回最近N天内该类型数据的记录列表。
    """
    if user_id not in health_data_store:
        return f"用户 {user_id} 暂无健康数据记录。"
    records = health_data_store[user_id]
    filtered = [r for r in records if r['type'] == data_type]
    filtered.sort(key=lambda x: x['timestamp'], reverse=True)
    recent = filtered[:days]
    if not recent:
        return f"最近{days}天内没有找到{data_type}的记录。"
    trend_str = "\n".join([f"{r['timestamp'][:10]}: {r['value']}" for r in recent])
    return f"用户 {user_id} 最近{days}天的{data_type}记录:\n{trend_str}"

tools = [record_health_data, get_health_trend]

# ---------- 第二部分:智能体与记忆配置 ----------
# 开发阶段开启缓存,节省成本
set_llm_cache(SQLiteCache(database_path=".health_agent_cache.db"))

llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.1)  # 温度调低,回答更稳定

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是专业的个人健康助手小健。你能帮助用户记录健康数据,并分析历史趋势。请友好、清晰地与用户交流。"),
    MessagesPlaceholder(variable_name="chat_history"),
    ("human", "{input}"),
    MessagesPlaceholder(variable_name="agent_scratchpad"),
])

agent = create_tool_calling_agent(llm=llm, prompt=prompt, tools=tools)
agent_executor = AgentExecutor(agent=agent, tools=tools, verbose=False, max_iterations=3)

# 记忆存储(生产环境应替换为Redis等)
memory_store: Dict[str, ChatMessageHistory] = {}

def get_session_history(session_id: str):
    if session_id not in memory_store:
        memory_store[session_id] = ChatMessageHistory()
    return memory_store[session_id]

agent_with_memory = RunnableWithMessageHistory(
    runnable=agent_executor,
    get_session_history=get_session_history,
    input_messages_key="input",
    history_messages_key="chat_history"
)

# ---------- 第三部分:模拟对话 ----------
def chat_with_agent(session_id="test_user"):
    print("健康助手小健已启动(输入'退出'结束)...")
    while True:
        try:
            user_input = input("\n你:")
            if user_input.lower() in ['退出', 'quit', 'exit']:
                print("小健:再见,保持健康!")
                break

            print("小健:", end="", flush=True)
            # 流式输出响应
            full_response = ""
            for chunk in agent_with_memory.stream(
                {"input": user_input},
                config={"configurable": {"session_id": session_id}}
            ):
                if "output" in chunk:
                    content = chunk["output"]
                    print(content, end="", flush=True)
                    full_response += content
            print()  # 流式输出完毕后换行

        except KeyboardInterrupt:
            print("\n\n对话被中断。")
            break
        except Exception as e:
            print(f"\n系统出现错误:{e}")

if __name__ == "__main__":
    # 在生产环境部署前,务必注释掉下面这行,关闭缓存!
    # set_llm_cache(None)
    chat_with_agent()

这个系统集成了我们讨论的所有核心要素:

  1. 工具化:定义了记录和查询健康数据的专用工具。
  2. 记忆化:通过RunnableWithMessageHistory管理多轮对话上下文。
  3. 流式化:使用stream方法实现答案的逐字输出,体验更佳。
  4. 可缓存:在开发阶段通过set_llm_cache节省API调用成本。

你可以运行这个脚本,体验一下完整的交互过程:

你:帮我记录一下今天的体重,70.5公斤。
小健:已记录:weight_kg = 70.5。备注:。
你:我上周的体重趋势怎么样?
小健:用户 test_user 最近7天的weight_kg记录:
2023-10-27: 70.5
(假设之前有记录...)
你:我的心率最近高吗?
小健:用户 test_user 暂无心率数据记录。你可以使用“记录心率”工具添加数据。

在实际部署时,你需要将内存中的memory_storehealth_data_store替换为真正的数据库(如PostgreSQL),并移除开发缓存。此外,考虑加入错误处理与重试机制(例如工具调用失败时让Agent重试或降级处理)、对话超时管理(清理长时间不活跃的会话)以及监控与日志(记录每个工具调用和LLM请求,便于问题排查和成本分析)。

走到这一步,你的LangChain Agent已经从一个简单的工具调用原型,成长为一个具备工程化雏形的智能体系统了。它知道如何思考并行动,能记住与用户的每一次交流,能够快速流畅地回应,并且在开发维护上更加经济高效。这为你构建更复杂的AI应用,比如智能客服、自动化工作流、数据分析助手,打下了坚实的地基。记住,所有的复杂系统都是从这样一个个可用的模块开始,逐步迭代、连接、扩展而成的。

更多推荐