Hermes + LangChain:从零做一个带记忆的聊天应用

用LangChain框架搭配Hermes本地模型,从零搭建一个带对话记忆的聊天应用。包含ConversationBufferMemory、链式调用等核心概念的实战代码。

目录

  1. 前置准备
  2. 最基础的对话:不带记忆
  3. 加上对话记忆
    1. ConversationBufferMemory
    2. ConversationBufferWindowMemory
    3. ConversationSummaryMemory
    4. ConversationSummaryBufferMemory
  4. 用 LCEL 构建更灵活的链
  5. 流式输出
  6. 加入工具调用
  7. 把它做成一个完整的聊天应用
  8. 持久化对话历史
  9. 进阶:检索增强生成(RAG)
  10. 踩坑记录
  11. 更多方向

做过 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 ChatOpenAI

# 创建模型实例,指向本地 Ollama
llm = 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 ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory

llm = 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 # 打开这个可以看到完整的 prompt
)

# 第一轮对话
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 ConversationBufferWindowMemory

# 只保留最近 5 轮
memory = 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 ConversationSummaryMemory

memory = 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 ConversationSummaryBufferMemory

memory = ConversationSummaryBufferMemory(
llm=llm,
max_token_limit=1000 # 超过这个 token 数的历史会被压缩
)

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 ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.messages import HumanMessage, AIMessage
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory

llm = ChatOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama",
model="hermes3:8b",
temperature=0.7
)

# 定义 prompt 模板
prompt = ChatPromptTemplate.from_messages([
("system", "你是一个专业的技术顾问,擅长给出实际可操作的建议。回答要具体,避免空泛的建议。"),
MessagesPlaceholder(variable_name="history"),
("human", "{input}")
])

# 构建链
chain = prompt | llm

# 会话存储(内存版本,生产环境可以换成 Redis)
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 ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

llm = 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 ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage

llm = 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
#!/usr/bin/env python3
"""带记忆的 Hermes 聊天应用"""

from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_community.chat_message_histories import ChatMessageHistory

def 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 RedisChatMessageHistory

def 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 SQLChatMessageHistory

def 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 ChatOpenAI
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 模型
llm = ChatOpenAI(
base_url="http://localhost:11434/v1",
api_key="ollama",
model="hermes3:8b"
)

# 嵌入模型(也用 Ollama 托管)
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()

# RAG prompt
prompt = ChatPromptTemplate.from_template("""
基于以下上下文回答问题。如果上下文中没有相关信息,请说明。

上下文:
{context}

问题: {question}
""")

# RAG 链
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 作为底层模型的靠谱程度,特别是在工具调用方面,完全能撑得起生产级的应用。

参与讨论

对这篇文章有疑问或想法?cocoloop 社区有不少开发者在讨论 Hermes 相关话题,欢迎加入交流。

前往 cocoloop 社区 →