做过 AI 应用的人都知道,大模型本身是无状态的——每次请求都是独立的,它不记得你上一轮说了什么。要让它”记住”对话历史,你得自己管理上下文。
手动管理当然可以,但写着写着你就会发现自己在重复造轮子:消息列表的维护、token 数的控制、系统提示词的注入……这些东西 LangChain 都帮你封装好了。
今天用 LangChain + Hermes 本地模型,搭一个带记忆的聊天应用。
前置准备 确保你已经在本地跑了 Hermes 模型。最简单的方式是用 Ollama 启动 Hermes :
1 2 ollama pull hermes3:8b ollama serve
然后安装 LangChain 相关的包:
1 pip install langchain langchain-openai langchain-community
注意这里装的是 langchain-openai,因为我们通过 OpenAI 兼容 API 来调用 Ollama 上的 Hermes。
最基础的对话:不带记忆 先从最简单的开始,看看 LangChain 怎么连 Hermes:
1 2 3 4 5 6 7 8 9 10 11 12 13 from langchain_openai import ChatOpenAIllm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0.7 ) response = llm.invoke("什么是 MapReduce?用大白话解释" ) print (response.content)
这跟直接调 API 没本质区别,只是套了一层 LangChain 的接口。真正有意思的在后面。
加上对话记忆 ConversationBufferMemory 最直觉的记忆方式——把所有对话历史原封不动地存下来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 from langchain_openai import ChatOpenAIfrom langchain.chains import ConversationChainfrom langchain.memory import ConversationBufferMemoryllm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0.7 ) memory = ConversationBufferMemory() conversation = ConversationChain( llm=llm, memory=memory, verbose=True ) print (conversation.predict(input ="我叫小明,我是一个后端工程师" ))print (conversation.predict(input ="根据我的背景,推荐我学习什么新技术" ))print (conversation.predict(input ="你刚才推荐的第一个,能具体说说怎么入门吗" ))
打开 verbose=True 之后,你能看到 LangChain 每次实际发给模型的完整 prompt。第二轮和第三轮的 prompt 里会包含之前所有的对话内容。
这种方式简单粗暴,但有个明显的问题:对话越长,prompt 越大,到后面会超出模型的上下文窗口。
ConversationBufferWindowMemory 保留最近 N 轮对话:
1 2 3 4 5 6 7 8 9 from langchain.memory import ConversationBufferWindowMemorymemory = ConversationBufferWindowMemory(k=5 ) conversation = ConversationChain( llm=llm, memory=memory )
适合闲聊场景,之前的上下文不太重要。
ConversationSummaryMemory 把历史对话压缩成摘要:
1 2 3 4 5 6 7 8 9 10 11 from langchain.memory import ConversationSummaryMemorymemory = ConversationSummaryMemory(llm=llm) conversation = ConversationChain( llm=llm, memory=memory ) print (memory.buffer)
这种方式更省 token,但摘要质量取决于模型能力。Hermes 在这方面表现不错,生成的摘要基本能抓住重点。
ConversationSummaryBufferMemory 混合模式——最近几轮保留原文,更早的压缩成摘要:
1 2 3 4 5 6 7 8 9 10 11 from langchain.memory import ConversationSummaryBufferMemorymemory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=1000 ) conversation = ConversationChain( llm=llm, memory=memory )
生产环境里最常用的方案。兼顾了近期对话的准确性和长期记忆的连续性。
用 LCEL 构建更灵活的链 LangChain 的新写法是 LCEL(LangChain Expression Language),比旧的 Chain 方式更灵活:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholderfrom langchain_core.messages import HumanMessage, AIMessagefrom langchain_core.runnables import RunnableWithMessageHistoryfrom langchain_community.chat_message_histories import ChatMessageHistoryllm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0.7 ) prompt = ChatPromptTemplate.from_messages([ ("system" , "你是一个专业的技术顾问,擅长给出实际可操作的建议。回答要具体,避免空泛的建议。" ), MessagesPlaceholder(variable_name="history" ), ("human" , "{input}" ) ]) chain = prompt | llm store = {} def get_session_history (session_id: str ): if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] chain_with_history = RunnableWithMessageHistory( chain, get_session_history, input_messages_key="input" , history_messages_key="history" ) config = {"configurable" : {"session_id" : "user_001" }} response1 = chain_with_history.invoke( {"input" : "我们团队想把 MySQL 迁移到 PostgreSQL,有什么建议?" }, config=config ) print (response1.content)response2 = chain_with_history.invoke( {"input" : "数据量大概 500GB,有什么迁移工具推荐?" }, config=config ) print (response2.content)
LCEL 的好处是你可以像搭积木一样组合各种组件,而且支持流式输出和异步调用。
流式输出 用户体验的关键——让回答逐字出来而不是等半天一次性蹦出来:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplatellm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0.7 , streaming=True ) prompt = ChatPromptTemplate.from_messages([ ("system" , "你是一个编程助手。" ), ("human" , "{input}" ) ]) chain = prompt | llm for chunk in chain.stream({"input" : "用 Python 写一个简单的 Web 爬虫" }): print (chunk.content, end="" , flush=True ) print ()
加入工具调用 Hermes 的 function calling 能力配合 LangChain 的 Tool 体系,可以让聊天机器人有”动手能力”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 from langchain_openai import ChatOpenAIfrom langchain_core.tools import toolfrom langchain_core.messages import HumanMessagellm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0 ) @tool def calculate (expression: str ) -> str : """计算数学表达式的结果。expression 是一个合法的 Python 数学表达式。""" try : result = eval (expression) return str (result) except Exception as e: return f"计算出错: {e} " @tool def get_word_count (text: str ) -> str : """统计文本的字数。""" return f"字数: {len (text)} " tools = [calculate, get_word_count] llm_with_tools = llm.bind_tools(tools) response = llm_with_tools.invoke([ HumanMessage(content="帮我算一下 (25 * 48) + (13 ** 3) 等于多少" ) ]) if response.tool_calls: for tc in response.tool_calls: print (f"工具: {tc['name' ]} " ) print (f"参数: {tc['args' ]} " ) if tc['name' ] == 'calculate' : result = calculate.invoke(tc['args' ]) print (f"结果: {result} " )
把它做成一个完整的聊天应用 整合上面所有的部分,做一个终端版的聊天 bot:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 """带记忆的 Hermes 聊天应用""" from langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholderfrom langchain_core.runnables import RunnableWithMessageHistoryfrom langchain_community.chat_message_histories import ChatMessageHistorydef create_chat_app (): llm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" , temperature=0.7 , streaming=True ) prompt = ChatPromptTemplate.from_messages([ ("system" , """你是一个友好且专业的AI助手。你的特点: 1. 回答简洁但有深度 2. 遇到不确定的事情会明确说明 3. 善于用例子和类比解释复杂概念 4. 记得用户之前提到的信息""" ), MessagesPlaceholder(variable_name="history" ), ("human" , "{input}" ) ]) chain = prompt | llm store = {} def get_history (session_id ): if session_id not in store: store[session_id] = ChatMessageHistory() return store[session_id] app = RunnableWithMessageHistory( chain, get_history, input_messages_key="input" , history_messages_key="history" ) return app def main (): app = create_chat_app() config = {"configurable" : {"session_id" : "default" }} print ("Hermes 聊天助手已启动(输入 quit 退出)" ) print ("-" * 50 ) while True : user_input = input ("\n你: " ).strip() if user_input.lower() in ["quit" , "exit" , "q" ]: print ("再见!" ) break if not user_input: continue print ("\nHermes: " , end="" , flush=True ) for chunk in app.stream( {"input" : user_input}, config=config ): print (chunk.content, end="" , flush=True ) print () if __name__ == "__main__" : main()
保存成 chat.py,然后 python chat.py 就能跑起来。
持久化对话历史 内存里的聊天记录重启就没了。如果你想持久化,可以用 Redis:
1 2 3 4 5 6 7 from langchain_community.chat_message_histories import RedisChatMessageHistorydef get_history (session_id ): return RedisChatMessageHistory( session_id=session_id, url="redis://localhost:6379" )
或者用 SQLite 做本地存储:
1 2 3 4 5 6 7 from langchain_community.chat_message_histories import SQLChatMessageHistorydef get_history (session_id ): return SQLChatMessageHistory( session_id=session_id, connection_string="sqlite:///chat_history.db" )
这样即使重启应用,之前的对话记录也还在。
进阶:检索增强生成(RAG) LangChain + Hermes 还能做一个简单的 RAG 系统。不过如果你想做更完整的文档问答,推荐去看 Hermes + LlamaIndex 搭建文档 QA 系统 那篇,LlamaIndex 在文档索引这块做得更专业。
这里简单示意一下 LangChain 的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 from langchain_openai import ChatOpenAIfrom langchain_community.embeddings import OllamaEmbeddingsfrom langchain_community.vectorstores import FAISSfrom langchain_core.prompts import ChatPromptTemplatefrom langchain_core.runnables import RunnablePassthroughfrom langchain_core.output_parsers import StrOutputParserllm = ChatOpenAI( base_url="http://localhost:11434/v1" , api_key="ollama" , model="hermes3:8b" ) embeddings = OllamaEmbeddings( base_url="http://localhost:11434" , model="nomic-embed-text" ) texts = [ "LangChain 是一个用于构建 LLM 应用的框架" , "vLLM 是一个高性能的 LLM 推理引擎" , "Hermes 是 Nous Research 开发的开源模型" , ] vectorstore = FAISS.from_texts(texts, embeddings) retriever = vectorstore.as_retriever() prompt = ChatPromptTemplate.from_template(""" 基于以下上下文回答问题。如果上下文中没有相关信息,请说明。 上下文: {context} 问题: {question} """ )rag_chain = ( {"context" : retriever, "question" : RunnablePassthrough()} | prompt | llm | StrOutputParser() ) answer = rag_chain.invoke("Hermes 是谁开发的?" ) print (answer)
踩坑记录 1. token 超限
Hermes 3 8B 默认在 Ollama 上的上下文是 2048 tokens,长对话容易超。在 Ollama 的 Modelfile 里把 num_ctx 调大,或者用 ConversationSummaryBufferMemory 控制历史长度。
2. 工具调用不稳定
Hermes 的 function calling 在复杂场景下偶尔会格式出错。建议 temperature 设低一点(0-0.3),并且在 tool 的 description 里把参数描述写清楚。
3. 流式输出和记忆冲突
用 RunnableWithMessageHistory 配合流式输出时,确保 LangChain 版本在 0.2 以上,旧版本有 bug。
更多方向 LangChain + Hermes 组合的能力远不止这些。如果你对多 Agent 协作感兴趣,可以看看 Hermes + AutoGen 的方案 。
cocoloop 社区里有不少人在用 LangChain 做各种有趣的项目,从智能客服到代码审查助手都有,感兴趣的话可以去交流交流。
从一个简单的聊天应用开始,逐步加上记忆、工具、检索……你会发现 LangChain 的抽象层确实能帮你省掉大量的胶水代码。而 Hermes 作为底层模型的靠谱程度,特别是在工具调用方面,完全能撑得起生产级的应用。