构建本地AI智能体:模型、记忆、存储与接口
本地AI智能体架构的五层结构,对比了Ollama、OpenClaw、Mem0以及Qdrant或Chroma,同时还修复了think-mode JSON格式问题及隐性维度不匹配问题。
来源: Building Local AI Agents: A Practical Guide to Models, Memory, and Orchestration
本地 AI 智能体将模型运行在您的硬件上,为您执行操作并在不同会话间保留上下文,无需将数据传输到外部 API。普通的聊天机器人只会回答问题然后遗忘。而智能体能够进行多步推理、使用工具,并随着时间积累知识。在本地运行可让您获得助手服务,同时避免托管服务带来的隐私问题及按次收费的弊端。
构建这样一个系统需要五个层面协同工作:在您的设备上执行推理的 LLM 层、负责调度和执行操作的智能体框架层、让智能体能力逐步提升的记忆层、用于存储其所获取信息的存储层,以及适配您现有工作方式的接口层。下文会详细介绍每一层的可选方案、权衡因素及推荐配置,帮助您组建适合自身需求的系统。
第 1 层:模型
这就是引擎。该模型能够识别意图、生成代码,并提取其他组件所依赖的结构化数据,其余的一切都是围绕其可靠功能构建的支撑系统。在本地推理领域,有三款工具占据主导地位。
Ollama 是最简单的入门方式:只需一条命令即可安装,另一条命令就能加载模型,这样你就能在本地主机上立即使用兼容 OpenAI 的 API。它无需额外配置即可管理模型、选择量化级别并设置上下文窗口。虽然它并非为高并发生产环境设计,但更适合作为个人助手或开发者工具使用。
ollama pull qwen3:8b
ollama pull nomic-embed-text
llama.cpp 的定位稍低一些。它是一个 C++ 引擎,可直接运行量化后的 GGUF 模型,无需守护进程也不涉及 HTTP 层。由于其内存占用极低,非常适合树莓派或嵌入式 Linux 等边缘设备使用,因为在这些设备上 Ollama 的体积会过大。不过它的缺点是缺乏内置的模型管理功能,需要更多手动配置。
vLLM 则是面向生产环境的方案。它采用了大规模云推理所使用的 PagedAttention 技术和连续批处理方式。如果您需要服务大量用户或具备较高的吞吐量需求,这就是合适的选择,但它要求具备完善的 GPU 基础设施,且扩展性优于前两者。
对于单个开发者的本地辅助工具而言,Ollama是合理的默认选择,因为简洁性比处理速度更为重要。qwen3:8b模型值得一提:它能够提供可靠的JSON结果用于意图识别,生成的代码结构清晰,且仅需8 GB的VRAM即可运行。需要注意的是,该模型会在实际内容之前用<think>…</think>标签包裹答案,这会干扰那些直接处理原始响应的JSON解析器。只需添加一个在所有位置设置think: False的简单封装即可解决此问题:
_orig_ollama_chat = ollama.chat
def _no_think_chat(*args, **kwargs):
opts = kwargs.get("options") or {}
if isinstance(opts, dict):
opts.setdefault("think", False)
kwargs["options"] = opts
return _orig_ollama_chat(*args, **kwargs)
ollama.chat = _no_think_chat
由于使用了 setdefault,各个调用方仍可在需要时重新启用思考模式。在工具流水线中的 JSON 提取和代码生成任务中,思考模式只会增加延迟而无法提升质量。
第二层:智能体框架
模型负责生成文本,而框架则决定如何处理这些文本以及下一步该做什么,涵盖意图分类、路由和执行等功能。
OpenClaw 以工具为核心,在文件系统层面运行。每个操作都是发送给已定义技能的命令,例如创建文件、编写代码、打开文件夹或运行 shell 命令。你需要编写一个 SKILL.md 文件来声明技能的名称及其调用方式,框架则负责处理路由逻辑:
- -
name: local-ai-assistant
description: Local AI coding assistant with persistent memory
command-dispatch: tool
command-tool: exec
command-arg-mode: raw
-
当大多数操作都是文件读写时,比如在代码辅助工具中,这种模型就很适用。command-dispatch: tool和command-tool: exec这两行指令会告诉OpenClaw在调用某个功能时执行Python脚本,这样文件操作就能通过该框架来完成。它的局限性在于并不适合处理冗长的多步骤推理链或协调多个智能体。
LangGraph则采取了相反的方法。你需要明确定义一个状态图,其中节点代表模型调用或动作,边代表状态转换,框架则负责在各个步骤之间传递状态。其代价是需要在开始任何业务逻辑之前就确定状态架构和图的结构。对于简单的工具使用来说这属于过度设计,但对于复杂的多步骤流程而言,它能提供每一步操作的可查看记录。
CrewAI 的层级更高。你可以为智能体定义角色与任务并附上描述,该框架会自动确定执行顺序及智能体间的通信方式。这是最快构建可运行的多智能体原型的方法,但这种抽象层使得故障调试更为困难。
无论选择哪种方法,每条接收到的消息都会首先进行意图分类。将基于模型的分类与正则表达式作为备用方案结合使用,比单独使用任一方法都能更好地应对故障:
def detect_intent(message: str) -> dict:
try:
resp = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": INTENT_PROMPT.format(message=message)}],
options={"temperature": 0, "num_predict": 1024},
)
return extract_json(resp["message"]["content"])
except Exception:
return keyword_intent_fallback(message)
当模型返回格式错误的输出时,回退机制会处理常见的异常情况。在关闭思考功能且温度值为0的情况下,大多数请求仍由模型路径处理,但正是回退机制让智能体能够持续运行。
第三层:记忆
记忆功能使得智能体既能根据你的习惯进行调整,又能将每次对话视为新的开始。它会学习你的惯例、偏好以及项目背景,并在无需提示的情况下自动应用这些信息。
常见的解决方案都在上下文窗口内部:启动时读取的 PREFERENCES.md 文件、冗长的系统提示,或是 CONTEXT.md 文件。这些方法都有一个共同缺陷——在窗口内部,它们会因内容压缩、令牌限制以及会话重启而被挤掉。实际上,智能体需要的是窗口之外的持久存储空间,能够根据需求有针对性地检索信息,并在事实发生变化时及时更新。有三种常见方法可满足不同的需求。
基础 RAG 是应用最广泛且对初学者最为友好的方案。它将文档或对话历史分割成块,嵌入这些块,当收到查询时再找出最相似的 top-k 个结果,几乎不需要额外设置。但其缺点是内存中会堆积大量文档,无法区分哪些内容值得长期保存,哪些只是临时性的查询。
会话级内存,例如 LangChain 的 ConversationBufferMemory 和 ConversationSummaryMemory,能够确保会话内的连贯性。智能体不会忘记五条消息之前你说过的话,而摘要功能则可防止信息量过多。不过这类内存在重启后会被清除,而这正是编码助手所面临的核心缺陷。
结构化持久内存,如Mem0所示,将内存视为一级数据而非原始文本。模型不会保留原文内容,而是提取出独立的事实,并将其嵌入到向量数据库中存储。检索时,查询语句会被嵌入数据库,系统会返回语义上相似的事实,即便新消息并未使用原有的表述方式。存储的事实会在不同会话之间保留,出现重复时会合并,而当现实情况变化时则会被重写。Mem0支持本地模型,因此很容易与基于Ollama的架构相连接。
以一位提到自己喜欢pytest的开发者为例:普通的RAG系统会保留原始句子,而结构化系统则会提取并存储这一偏好本身。下次当你请求带有测试功能的函数时,即使消息中从未提及具体的测试框架,内存也会返回“用户偏好pytest”这一信息。
将 Mem0 连接到本地 Ollama 环境只需进行简单配置,指定本地模型、本地嵌入模型以及向量存储的路径即可:
from mem0 import Memory
config = {
"llm": {
"provider": "ollama",
"config": {"model": "qwen3:8b", "ollama_base_url": "http://localhost:11434"},
},
"embedder": {
"provider": "ollama",
"config": {"model": "nomic-embed-text", "ollama_base_url": "http://localhost:11434"},
},
"vector_store": {
"provider": "qdrant",
"config": {"host": "localhost", "port": 6333, "embedding_model_dims": 768},
},
}
memory = Memory.from_config(config)
memory.add("I always use type hints and pytest", user_id="dev")
results = memory.search("write a utility function", user_id="dev")
llm模块将Mem0指向qwen3:8b以进行事实提取,从而将原始消息转换为离散的存储偏好项,而非原文文本。embedder模块在写入时使用nomic-embed-text将这些事实向量化,在检索时则用于嵌入查询,随后memory.search()会根据语义而非关键词进行检索。
对于任何记忆系统而言,一个常被忽视的改进措施就是在写入数据之前先对其进行过滤。在每次写入操作前运行一个轻量级分类器,可以保持记忆系统的整洁并提升检索质量:
def _is_worth_storing(self, user_message: str) -> bool:
response = ollama.chat(
model=OLLAMA_CHAT_MODEL,
messages=[{"role": "user", "content": SMART_MEMORY_PROMPT.format(
user_message=user_message
)}],
options={"temperature": 0, "num_predict": 512},
)
data = self._extract_json_robust(response["message"]["content"])
return bool(data.get("worth_storing", False))
其提示为模型提供了区分这两者的明确示例:
HIGH-VALUE (worth storing):
- "I prefer TypeScript over JavaScript"
- "I use pytest, never unittest"
- "Always use Google-style docstrings"
LOW-VALUE (discard):
- "What does enumerate() do?"
- "Write a retry decorator"
- "Thanks"
如果不使用过滤功能,向量数据库会逐渐被一次性请求填满,从而降低检索效率。
第4层:存储
内存层负责决定要保存什么内容,而存储则是指其在磁盘上的存放位置。
FAISS是一个进程内库:速度快、经过充分测试且无需依赖任何基础设施。它可以嵌入到Python进程中在内存中运行,也可以被序列化到磁盘上。它的局限性主要体现在操作层面:持久化需要显式的序列化操作,且没有HTTP API或内置的复制功能。这虽然适合研究流程和批量处理任务,但对于需要频繁重启的长期运行的助手来说则不够稳定。
Chroma 是开发者所能选择的最为简单的向量数据库。它最初以本地 Python 进程的形式运行,或嵌入到用户的代码中,同时会自动将数据保存到本地目录。可通过以下命令进行安装:
pip install chromadb
在个人助手中存储数千条需记住的事实完全在Chroma的能力范围内。它彻底消除了对基础设施的依赖,但过滤功能相对有限,不如Qdrant强大。
Qdrant作为真正的服务运行,具备完整的REST和gRPC接口、数据负载过滤功能以及命名向量支持。它适用于需要处理数百万个向量及大量并发查询的场景。该系统可原生持久化到磁盘上,无需特殊处理即可在Docker重启后继续正常运行:
# Qdrant via Docker
docker run -d - name qdrant-local -p 6333:6333 \
-v $(pwd)/qdrant_storage:/qdrant/storage qdrant/qdrant
有一个配置细节会悄悄导致问题:向量存储配置中的嵌入维度必须与你的嵌入模型的输出维度完全一致。例如,nomic-embed-text 模型生成的向量维度为768。如果在不修改该数值的情况下更换嵌入模型,插入操作将会失败,但不会显示任何错误信息。
"vector_store": {
"provider": "qdrant",
"config": {
"collection_name": "coding_assistant",
"host": "localhost",
"port": 6333,
"embedding_model_dims": 768, # must match your embedding model exactly
},
}
如果想要放弃 Docker,Chroma 可以直接替代:只需修改配置中的 vector_store 提供商,其余组件保持不变即可。
第 5 层:接口
最外层的接口决定了你与代理的交互方式,具体选择取决于它在工作流中的位置。
- Web UI。 最具灵活性的选择。它在浏览器中运行,能够渲染 Markdown、高亮代码及可折叠区域,且无需安装即可在任何操作系统上使用。编码助手可将当前文件、生成的代码以及代理的内存状态统一显示在同一界面中。
这些选择如何相互配合
每一层都会对其相邻层施加约束。模型决定框架调用的 API 形式,内存管理策略决定了需要支持何种存储方式,而接口则决定了输出格式的重要性程度。
对于单个开发者的本地编码助手而言,这种技术架构在降低使用门槛的同时确保每一层都可以被替换:
Interface: Web UI on localhost
│
Framework: OpenClaw - tool-dispatch, SKILL.md routing
│
LLM: Ollama for qwen3:8b
(think mode disabled for JSON reliability)
│
Memory: Structured persistent memory with value filtering
Embeddings via nomic-embed-text
│
Storage: Chroma (zero infrastructure) or Qdrant (more robust)
由于每一层都可以独立更换,因此应在每个层级从最简单的选项开始,随着需求增长再逐步升级。从 Chroma 更换到 Qdrant,或从 Ollama 更换到 vLLM,都无需重新构建其上下层的内容。如果有一层值得尽早投入资源,那就是内存部分。一个拥有精心设计的内存层的中小型本地模型,其性能远优于那些每次会话都从零开始运行的大型模型。