《当知识开始“长记性”:一个小型 AI 知识库是怎样醒过来的》
想象一下,你有一位记性超好的助理:跟 TA 讲过的事情,过几天再问,还能带着上下文清清楚楚地复述给你听。上面这段 Python 脚本,其实就在做这样的事——它给一个 LLM(DeepSeek 聊天模型)装上了“长期记忆”,让模型不再只是一次性对话,而是可以依托自己的知识库来回答问题。
下面我们拆开来看,这个“小型 AI 知识库系统”是如何一步一步搭起来的。
---
🧱 整体结构:三块积木拼出一个“有记忆”的 Agent
从代码结构上看,一共三块核心组件:
1. 嵌入模型(Embedding) + 向量数据库(Chroma) 负责把文本“压缩”成向量,并存入一个可持久化的向量库。 2. 知识库封装(Knowledge + SqliteDb + ChromaDb) 负责统一管理“原始文本内容 + 向量索引”。 3. Agent(DeepSeek 聊天模型 + Knowledge) 负责真正和用户对话、从知识库中检索信息并组织回答。
主流程在 main() 里:
def main():
# 1. 创建知识库(向量库 + 内容库)
knowledge_base = create_knowledge_base()
# 2. 创建 DeepSeek 聊天模型
model = create_agent()
# 3. 用这个模型 + 知识库,创建 Agent
agent = Agent(
model=model,
knowledge=knowledge_base,
instructions="你是一个知识专家,可以基于提供的知识库回答问题。",
)
# 4. 往知识库里塞几条示例知识
...
# 5. 让 Agent 基于知识库回答问题
...
# 6. 直接对知识库做搜索
...
这就相当于:
create_knowledge_base()= 给助理准备“记忆系统”;create_agent()= 找一个会说话的大脑(LLM);Agent(...)= 把这俩组装成能聊天、又有记忆的“全职助理”。
🧬 本地嵌入模型:给文字“测 DNA”
1. 为什么需要“嵌入(Embedding)”?
向量数据库不是直接存字符串,而是存向量。 所以必须先把文本变成向量,这个过程就像是:对每句话做一次“DNA 测试”,输出一串能代表语义的数字序列。
在代码中,这一步由本地的 SentenceTransformerEmbedder 完成:
from agno.knowledge.embedder.sentence_transformer import SentenceTransformerEmbedder
embedder = SentenceTransformerEmbedder(
id="sentence-transformers/all-MiniLM-L6-v2", # 轻量级模型,适合中文
dimensions=384, # 该模型的输出维度
)
> 注解
> Sentence Transformers 是一个专门做文本向量化的模型家族,all-MiniLM-L6-v2 是其中相对轻量的一款。
> 384 维向量意味着:每段文本会被映射到一个 384 维的“语义坐标系”中。
2. 完全本地,无需 API
这里的嵌入模型是本地加载,没有调用任何云端 API:
- 优点:不依赖网络,不消耗外部 API 额度,隐私友好;
- 代价:机器上要能装得下模型(但这个模型非常小,一般电脑可以轻松跑)。
except ImportError:
print("❌ 未安装 sentence-transformers 包")
print("请运行: pip install sentence-transformers")
return None
except Exception as e:
print(f"❌ 加载嵌入模型失败: {e}")
return None
这相当于在启动“记忆系统”之前先检查下:我有没有给自己装上“理解语义的眼镜”?
---
🗂️ Chroma 向量数据库:给记忆找个“仓库”
1. 创建 ChromaDb 实例
有了嵌入器,下一步就是找个地方放向量。这就是 Chroma 的工作:
vector_db = ChromaDb(
collection="knowledge_base",
path="tmp/chromadb",
persistent_client=True, # 启用持久化存储
embedder=embedder,
)
几个关键信息:
collection="knowledge_base":相当于一个“知识库名字空间”,方便分类管理。path="tmp/chromadb":向量索引会持久化在这个目录里,下次运行还能用。persistent_client=True:开启持久化,而不是每次内存临时玩一把就扔了。embedder=embedder:告诉 Chroma:怎么把文本变成向量,由这个嵌入器说了算。
---
📚 Knowledge:把“向量索引 + 原文内容”绑在一起
只存向量还不够,用户问问题时,你得能把原始文本拿出来展示。这就是 Knowledge 的职责:
knowledge_base = Knowledge(
vector_db=vector_db,
contents_db=SqliteDb(db_file="knowledge_contents.db")
)
这里用了两个“数据库”:
1. vector_db:Chroma,用来高效做相似度搜索;
2. contents_db:SQLite,用来存每一条知识的原始文本内容。
你可以把它想象成:
- Chroma 是“目录卡片 + 文本特征”;
- SQLite 是“真正的书本内容”;
Knowledge是那位熟悉图书馆结构的管理员,帮你统一操作。
代码中的两个典型操作
1. 添加知识(add_content):
for i, text in enumerate(sample_texts):
knowledge_base.add_content(text_content=text)
背后流程大致是:
- 调用 embedder → 文本转向量;
- 向量写入 Chroma;
- 文本写入 SQLite;
- 建立 ID 关联。
search):search_results = knowledge_base.search("编程语言", max_results=2)
for doc in search_results:
print(doc.content[:100], "...")
背后流程大致是:
- 把查询
"编程语言"做嵌入 → 得到查询向量; - 在 Chroma 中做近邻搜索 → 找最相近的几条向量;
- 根据 ID 从 SQLite 拉回原始文本;
- 以
doc的形式返回给你。
🤖 DeepSeek 聊天模型:给系统安上“嘴”和“脑子”
前面我们解决的是“记忆”和“检索”,现在需要一个会说话、会推理的主体,这里用的是 DeepSeek 聊天模型:
from agno.models.deepseek import DeepSeek
def create_agent():
api_key = os.getenv("DEEPSEEK_API_KEY") or os.getenv("OPENAI_API_KEY")
model = DeepSeek(
id="deepseek-chat",
api_key=api_key,
)
return model
这里的逻辑:
- 优先从环境变量中拿
DEEPSEEK_API_KEY; - 如果没有,就尝试
OPENAI_API_KEY(兼容某些代理/转发场景); - 拿不到的话照样创建模型,只是可能会在请求时失败。
---
🧠 Agent:当大脑学会“先查资料,再开口”
1. 组装 Agent
agent = Agent(
model=model,
knowledge=knowledge_base,
instructions="你是一个知识专家,可以基于提供的知识库回答问题。",
)
这里,Agent 扮演的角色就是:
- 收到一个用户问题;
- 先去知识库里检索相关内容;
- 再把这些内容 + 问题,一起丢给 DeepSeek 模型;
- 用 prompt 里的角色设定引导模型:“你是知识专家,要基于提供的知识库回答”。
2. 使用 Agent 回答问题
agent.print_response("请介绍一下 Python 的历史。")
按理想路径,底层流程大致是:
1. 对问题做嵌入,去 knowledge_base 搜索相关文本;
2. 把检索到的一两段关于 Python 的说明作为“上下文”;
3. 再把这些上下文 + 原问题,丢给 DeepSeek;
4. DeepSeek 结合上下文生成一段更流畅、结构化的中文回答;
5. print_response 负责把这段回答打印出来。
> 注解 > 这就是典型的 RAG(Retrieval-Augmented Generation,检索增强生成)模式: > 模型不再“凭空想象”,而是先去你专门指定的知识库查资料,再组织回答。
---
🔍 直接搜索:验证知识库是否“记住了东西”
最后一段是直接对知识库做检索:
search_results = knowledge_base.search("编程语言", max_results=2)
print(f"找到 {len(search_results)} 条相关结果:")
for i, doc in enumerate(search_results):
print(f"{i+1}. {doc.content[:100]}...")
在前面添加的示例文本中,有三条:
- Python 是一种高级编程语言…
- JavaScript 是一种脚本语言…
- 机器学习是人工智能的一个分支…
---
🛠 可以考虑的改进与拓展
这段脚本已经是一个完整、可跑的小 demo,不过从“科研工程师”的角度看,可以扩展的地方还有不少:
1. 更完善的错误处理与日志
现在大部分错误是 print 一行,然后 return None。在真实系统里可以:
- 区分“致命错误”(比如向量库不可写)和“可恢复错误”(比如某条内容失败);
- 使用 logging 模块,打 INFO / WARNING / ERROR 级别日志;
- 对知识添加失败时,记录文本 ID,方便后续重试。
2. 更适合中文的嵌入模型
all-MiniLM-L6-v2 虽然也能处理中文,但不是专门为中文设计。
对于中文知识库,可以考虑:
sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2(多语言);- 或本地的中文专用 Embedding 模型(如 bge-m3/bge-large-zh 等,视 Agno 支持情况)。
id="xxx-模型名",
dimensions=向量维度,
即可。
3. 知识分片与元数据
当前示例用的是“几句话直接当一条内容”。现实中:
- 文档往往很长,需要按段落/小节切分成多条;
- 每条内容需要额外的 metadata:来源文件名、章节、时间戳等;
- 搜索时可以按 metadata 过滤,比如只查“某个项目的文档”、“某个时间之后的内容”。
add_content 支持 metadata,可以这样扩展:knowledge_base.add_content(
text_content=text,
metadata={"source": "demo", "topic": "programming"}
)
4. Agent 的指令(instructions)可以更精细
现在的 instructions 很简单:
instructions="你是一个知识专家,可以基于提供的知识库回答问题。"
可以更具体一些,例如:
- 优先引用知识库内容,如无相关内容要明确说明“知识库中暂无此信息”;
- 回答时用分点说明,标明知识来源;
- 避免超出知识库范围的主观猜测。