从十年 Java 到智能体开发:我的全套 Python 智能体实战课程(第二帖)5-记忆与提示词
对了,有个事情之前忘了说了,大家的deepseek记得要充钱哦,我的是有余额的,所以之前没在意,不好意思!
上一篇我们说了llm的接口调用,本篇我们来实战一下。之前的项目只是一个llm接口调用,并不能算一个真正的智能体,接下来我们通过创建智能体的方式修改代码
我们创建py文件,名字 main_agent :
# 从 langchain_openai 库导入 ChatOpenAI 类,用于调用兼容 OpenAI 接口的大模型(如 DeepSeek) from langchain_core.messages import AIMessage from langchain_openai import ChatOpenAI # 从 langchain.agents 中导入 create_agent 函数,这是 LangChain 1.0 官方推荐的创建智能体的标准入口 from langchain.agents import create_agent # 从 langgraph.checkpoint.memory 导入 MemorySaver 类,用于将对话状态存储在内存中(仅在程序运行期间有效) from langgraph.checkpoint.memory import MemorySaver # 从当前目录的 config.py 文件中导入 Config 类,用于读取 API 密钥等配置 from config import Config from http_utils import LoggingHttpClient # 调用 Config 类的 validate 方法,检查 DEEPSEEK_API_KEY 是否已正确设置,若未设置则抛出异常 Config.validate() # 创建带日志的客户端,传入 ChatOpenAI http_client: LoggingHttpClient = LoggingHttpClient(timeout=60.0) # 可自定义超时 # 初始化 ChatOpenAI 实例,用于与 DeepSeek API 通信,传入模型名称、密钥、基础地址和温度参数 llm = ChatOpenAI( model=Config.LLM_MODEL, # 模型名称,从配置中读取 api_key=Config.DEEPSEEK_API_KEY, # API 密钥,从配置中读取 base_url=Config.DEEPSEEK_BASE_URL, # API 基础地址,从配置中读取 temperature=0.7, # 温度参数,控制回答的随机性(0~1) http_client=http_client ) # 创建 MemorySaver 实例,用于在内存中存储对话的检查点(checkpoint),即对话历史 memory = MemorySaver() # 调用 create_agent 函数创建一个智能体(Agent),它内部基于 LangGraph 构建,具备工具调用和多轮对话能力 agent = create_agent( model=llm, # 指定智能体使用的大模型实例 tools=[], # 工具列表,这里为空,表示没有额外工具 system_prompt="你是一个智能体助手,名字叫小智", # 系统提示词,设定智能体的角色和行为准则 checkpointer=memory # 传入 checkpointer(即记忆组件),使智能体能够记住历史对话 ) # 打印启动界面的分隔线和提示信息 print("=" * 40) print("DocMind 智能体(带记忆)") print("输入 'exit' 退出") print("=" * 40) # 定义一个固定的会话线程 ID,用于标识同一用户的对话,从而实现持久记忆 thread_id = "user_001" # 进入无限循环,不断接收用户输入并返回智能体的回复 while True: # 提示用户输入,并读取用户输入的内容 user_input = input("\n你: ") # 如果用户输入的小写形式是 "exit",则跳出循环,结束程序 if user_input.lower() == "exit": break # 如果用户输入的是空字符串或仅包含空白字符,则跳过本次循环(不处理) if not user_input.strip(): continue # 调用智能体的 invoke 方法,传入用户消息和配置,以获取回复 result = agent.invoke( {"messages": [{"role": "user", "content": user_input}]}, # 将用户输入包装成消息列表,角色为 user config={"configurable": {"thread_id": thread_id}} # 通过 config 指定 thread_id,确保同一线程共享记忆 ) print("=" * 40) print(f"响应对象:{result}") # 从返回结果中提取最后一条消息(即智能体的回复)的内容,并打印出来 print("=" * 40) # --- 稳健获取 AI 回复(仅取纯文本的 AIMessage) --- last_ai_msg = None # 从后往前遍历,找到最后一个真正的 AIMessage 且 content 非空 for msg in reversed(result['messages']): if isinstance(msg, AIMessage) and msg.content: last_ai_msg = msg break if last_ai_msg: print(f"AI: {last_ai_msg.content}") else: print("AI: (未获取到有效回复)")有两个点需要注意一下:
第一点,我们添加了一个http请求的日志,为了验证我们上一篇的接口调用,查看我们的请求报文,需要添加py文件,名字为 http_utils :
import httpx import json # 定义一个自定义的 HTTP 客户端,重写 send 方法以打印请求体 class LoggingHttpClient(httpx.Client): def send(self, request, *args, **kwargs): # 打印请求 URL 和 headers(可选) print(f"🔗 请求 URL: {request.url}") print(f"📋 Headers: {request.headers}") # 打印请求体(body) if request.content: try: body_json = json.loads(request.content) print("📤 原始请求体 (JSON):") print(json.dumps(body_json, indent=2, ensure_ascii=False)) except: print("📤 原始请求体 (文本):", request.content.decode()) # 继续正常发送请求 return super().send(request, *args, **kwargs)这样我们就可以看到如下的请求接口内容:
📤 原始请求体 (JSON):
{
"messages": [
{
"content": "你是一个智能体助手,名字叫小智",
"role": "system"
},
{
"content": "我是阿岳你是谁",
"role": "user"
},
{
"content": "你好啊,阿岳!我是你的智能助手小智,随时准备为你解答问题、提供建议或者陪你聊天。有什么可以帮你的吗?😊",
"role": "assistant"
},
{
"content": "我喜欢布偶猫,你可以说一下它的优点吗",
"role": "user"
},
{
"content": "布偶猫简直就是猫中小天使!先说颜值:它们有湛蓝的眼睛像宝石,柔软的长毛摸起来像云朵,尤其是那个标志性的“布偶”体型,抱在怀里特别有分量感。性格更是一绝——不仅粘人还超级温柔,你回家它会迎门口,你一坐下它就赖腿上求摸,甚至被抱着揉肚子都不会伸爪子。还有它的“社交达人”属性,家里来客人也能淡定撒娇,几乎不搞破坏。养一只布偶猫,相当于同时拥有毛绒玩具+暖心小跟班哦!🐱💙",
"role": "assistant"
},
{
"content": "有多暖心",
"role": "user"
}
],
"model": "deepseek-v4-flash",
"stream": false,
"temperature": 0.7
}
大家可以看到,请求的数据结构,与我们上一篇讲的一致,而且因为我们添加了系统提示词,所以llm知道他叫小智,还有就是我的问题 "有多暖心" ,这句话单独拿出来语义是很割裂的,如果没有上下文的话,大模型根本不会回答的这么准确,上面的请求返回如下:
AI: 当然,阿岳!布偶猫的“暖心”是刻在骨子里的,给你几个真实感极强的例子——
1. **“膝盖雷达”模式**:你刚坐下沙发,它会慢悠悠走过来,用脑袋蹭你的手,然后“啪叽”一声瘫在你大腿上,像个小毛毯一样团起来。哪怕你打游戏、看书,它也要贴着你,偶尔抬头对你“喵”一声,仿佛在说:“别太累啦,我陪着你呢。”
2. **你的情绪侦探**:如果你加班回家倒头就躺,它会跳上床,用小爪子轻轻拍你的脸,或者把毛茸茸的头枕在你肩膀上,咕噜咕噜地“按摩”式踩奶。有次我朋友心情不好,她家布偶猫居然叼来最爱的玩具,放在她枕头边,眼神湿漉漉地看着她。
3. **温柔的“跟屁虫”**:你上厕所它蹲在门口,你做饭它趴在厨房地垫上,甚至你洗澡时它也守着玻璃门——不是捣乱,就是想确认你在视线里。半夜翻身找不到它?别担心,它可能正缩在你脖子边,呼噜声比助眠白噪音还治愈。
4. **顶级“暖宝宝”**:冬天它简直是移动暖水袋!尤其喜欢钻被窝,钻进你的臂弯里睡觉,体温加上柔软的长毛,抱着它就像抱着会呼吸的毛绒小熊。最戳人的是,你假装生气时,它会用脑袋顶你的下巴,或者翻肚皮假装“认输”求原谅。
**总结**:布偶猫的暖不是“讨好”,而是它们天生信任人类,把陪伴当成本能。养久了你会觉得——这猫怕不是觉得自己是个人类幼崽吧?😊
(要听它撒娇的小故事吗?我还可以再编个番外篇~)
第二点,就是我们在代码中,打印了result对象,这是为了我们更好的拿出llm给我们返回的内容:
响应对象:{'messages': [HumanMessage(content='我是阿岳你是谁', additional_kwargs={}, response_metadata={}, id='77506947-55b5-4bdd-9648-47e3046e2aed'), AIMessage(content='你好啊,阿岳!我是你的智能助手小智,随时准备为你解答问题、提供建议或者陪你聊天。有什么可以帮你的吗?😊', additional_kwargs={'refusal': None}, response_metadata={'token_usage': {'completion_tokens': 113, 'prompt_tokens': 18, 'total_tokens': 131, 'completion_tokens_details': {'accepted_prediction_tokens': None, 'audio_tokens': None, 'reasoning_tokens': 79, 'rejected_prediction_tokens': None}, 'prompt_tokens_details': {'audio_tokens': None, 'cache_write_tokens': None, 'cached_tokens': 0}, 'prompt_cache_hit_tokens': 0, 'prompt_cache_miss_tokens': 18}, 'model_provider': 'openai', 'model_name': 'deepseek-v4-flash', 'system_fingerprint': 'fp_8b330d02d0_prod0820_fp8_kvcache_20260402', 'id': 'eddb20cb-cc91-4d75-9215-337cf2819c37', 'finish_reason': 'stop', 'logprobs': None}, id='lc_run--019f573f-5b22-72b2-8192-f037ad2d3538-0', tool_calls=[], invalid_tool_calls=[], usage_metadata={'input_tokens': 18, 'output_tokens': 113, 'total_tokens': 131, 'input_token_details': {'cache_read': 0}, 'output_token_details': {'reasoning': 79}})]}
这行代码 if isinstance(msg, AIMessage) and msg.content: 就是我们判断拿到类型为AIMessage且不为空的消息。这行代码的上一行的reversed(...)方法:这是Python的内置函数,作用是反转迭代顺序。它不会改动原来的列表,只是让你在遍历时,从列表的最后一个元素开始往前取。因为ai返回的都是在消息后面部分。
最后总结一下:llm的接口每次都是单独http请求调用的,也就是说,其实所谓的记忆,都是每次单次调用接口的时候我们加上去的,是我们人为给大模型构造出的记忆。