如果你最近也在琢磨“智能体(Agent)到底怎么落地”,大概率会先搜到一大堆平台、框架、提示词技巧。但真正动手做一个就会发现:大部分教程只教你“怎么让模型开口说话”,很少有人讲清楚“怎么让Agent记住该记住的背景信息”。
说实话,做一个能回答“谁发明了钢琴键背景智能体”这类问题的知识型Agent,听起来像玩具,但它几乎把智能体开发的整套骨架都过了一遍:背景知识从哪来、怎么结构化、存到哪里、对话时如何唤起、答错如何修正。这比单纯聊概念更有价值。
这篇文章不会只讲概念,而是用一个能跑通的最小示例,把“背景知识的注入与记忆”这件事完整拆开。读完你至少能回答三个问题:第一,知识型智能体和普通聊天机器人差在哪;第二,背景知识为什么不能一股脑塞进提示词;第三,如果不用大而全的平台,自己用代码搭一个需要哪些模块。
1. 这篇文章真正要解决的问题
很多初学者对智能体的理解是“接一个大模型API,写一段Prompt,就能回答问题了”。这个认知在简单闲聊场景下没错,但一旦涉及特定领域知识,问题马上出现。
以“谁发明了钢琴键”这个需求为例。你直接问大模型,它也能回答出一段关于钢琴发明史的话。可如果这个智能体服务的不是普通用户,而是音乐教育产品里的内置助手呢?它需要:
第一,回答口径要和产品的知识点体系一致,不能每次生成都不稳定。第二,它要能记住用户之前问过什么、看到过哪些知识,而不是每次都从零开始。第三,知识库更新时,Agent要能及时用上新内容,而不是依赖模型训练时的旧数据。
这三个需求,分别对应智能体工程里的知识注入、记忆管理和知识更新。它们不是“加分项”,而是把一个Demo变成可用产品的门槛。
所以这篇文章真正要解决的,不是“怎么调API”,而是“怎么让Agent拥有稳定的领域背景认知”。准确说,就是回答一个问题:当模型本身不具备某个背景知识时,Agent如何通过工程手段把它补上,并且记住、用对。
2. 智能体的核心概念与背景记忆原理
2.1 什么是智能体
智能体,英文叫Agent,本质上是一个“能调用外部能力来完成任务的AI系统”。它不等于大模型,而是以大模型为大脑,在外面套了工具、知识和流程。
一个完整的智能体至少包含四个部分:
第一部分是模型,负责理解和生成。第二部分是提示词与上下文,负责告诉模型“你是什么角色、该按什么规则回答”。第三部分是知识库或工具,负责给模型补充实时信息。第四部分是记忆,负责跨轮次记住对话状态和用户偏好。
很多教程会把Agent讲得很玄,实际上它与普通聊天机器人的差别就一句话:聊天机器人只说,Agent会查、会算、会记住、会执行。
2.2 背景知识、上下文窗口与记忆
这里有三组容易混淆的概念,建议先分清楚。
第一组是“背景知识”和“上下文窗口”。背景知识是Agent需要掌握但模型不一定知道的信息,比如钢琴键的发明历史、产品规则、内部文档;上下文窗口是单次请求中能塞给模型的最大文本量。把背景知识塞进上下文,是让模型“临时知道”,但窗口有限,塞太多又会挤占推理空间。
第二组是“短时记忆”和“长期记忆”。短时记忆指当前对话里的历史消息,一般直接放在上下文里;长期记忆指跨会话保存的用户信息和知识要点,通常需要落到外部存储。
第三组是“参数化知识”和“非参数化知识”。参数化知识是训练时学进模型权重里的知识;非参数化知识是模型通过检索或工具临时拿到的外部知识。对业务型Agent来说,非参数化知识往往更可控。
可以简单对比一下:
| 概念 | 通俗理解 | 常见存储位置 |
|---|---|---|
| 背景知识 | Agent需要知道的领域事实 | 知识库、文档、JSON文件 |
| 短时记忆 | 这一轮对话说了什么 | 请求上下文 |
| 长期记忆 | 用户之前关心过什么 | 数据库、向量库 |
| 参数化知识 | 模型天生就会的 | 模型权重 |
| 非参数化知识 | 运行时临时获取的 | 外部检索结果 |
2.3 三种知识注入方式的对比
让Agent获得背景知识,主流有三种方式:
第一种是直接写进System Prompt。适合知识量小、变化不频繁的场景。优点是实现简单,缺点是知识一多就超过窗口限制,更新还要发新版本。
第二种是RAG检索增强生成。把背景知识切分成片段,用户提问时先检索相关片段,再拼进提示词。优点是可以放大量知识,更新知识库不用改代码;缺点是工程复杂度高,检索质量直接影响回答效果。
第三种是微调模型。让模型通过训练把知识学进权重。优点是回答自然,但成本高、周期长,也不是“记住业务背景”的首选。
对“谁发明了钢琴键背景智能体”这类中小规模知识型Agent,更务实的做法是“Prompt + 结构化知识文件 + 简单记忆模块”。下面整个示例都按这个路线实现。
3. 环境准备与前置条件
这个项目的代码非常简单,不需要复杂平台。推荐直接用 Python 开发,因为后续接大模型API、做检索、连向量库都很方便。
建议环境如下:
- Python 3.9 及以上版本。
- 一个文本编辑器或 IDE,比如 VS Code。
- 可以用虚拟环境管理依赖,避免污染系统环境。
- 如果需要真正接大模型,请自备一个兼容 OpenAI 风格的 API 服务。本文代码会保留一个模型调用入口,但核心演示不依赖任何特定厂家的接口。
- 如果你对代码不熟,也可以先在 Dify、Coze 这类智能体平台上手动拖拽实现同样流程。本文第5章的代码和平台上的节点一一对应。
先说清楚一点:版本号请以你实际安装的为准。本文重点不是某个库的API,而是“背景知识注入与记忆”的通用思路。
创建虚拟环境并安装必要依赖:
mkdir piano-agent cd piano-agent python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install openai这里的openai库只是用来演示大模型调用入口。如果你用的不是 OpenAI,只要你的服务提供兼容接口,SDK 基本是同一个模式。后面代码里我会用一个chat_once函数把调用隔离出来,方便替换。
4. 核心流程拆解
4.1 明确需求与知识边界
做智能体之前,第一步不是写代码,而是把需求说清楚。
“谁发明了钢琴键背景智能体”这个需求,可以拆成三条:
- 用户询问“谁发明了钢琴键”时,Agent 要基于预设背景知识回答,而不是自由发挥。
- Agent 要记住对话里出现的用户身份、偏好和已展示过的内容。
- Agent 知识更新后,回答内容要跟着变,而不是咬死旧数据。
这里有个很容易犯的错:认为背景知识就是“写几句历史简介塞进提示词”。实际上,背景知识是可以被查询的结构化数据。真实项目里,它可能是产品文档、工单记录、数据库字段说明,甚至是一份Excel表格。
4.2 设计背景知识库
背景知识库的作用,是给Agent提供可检索、可更新的“领域事实”。
钢琴发明史这个例子,背景知识库可以设计成这样一个结构:
{ "topic": "piano_keyboard_history", "facts": [ { "id": "fact_001", "question": "谁发明了现代钢琴", "answer": "意大利人巴托罗密欧·克里斯托弗里(Bartolomeo Cristofori)在1700年前后发明了现代钢琴。", "tags": ["钢琴", "发明者", "克里斯托弗里"] }, { "id": "fact_002", "question": "钢琴键盘的前身是什么", "answer": "钢琴键盘的雏形可以追溯到管风琴和羽管键琴的键盘结构。", "tags": ["钢琴", "键盘", "前身"] } ] }这个设计有三个要点:
第一,每条知识都有独立ID,方便更新和追踪。第二,问题和答案分开存,检索时可以用问题做匹配,回答时取答案。第三,tags字段用于模糊匹配,解决用户问法不标准的问题。
真实场景里你不需要做得这么简单,但最小示例的关键是“知识可以被程序读取”,而不是写在人读的文档里。
4.3 选择记忆存储方案
记忆模块需要解决一个核心问题:用户上次问过什么,这个会话结束之后还要不要记住?
如果只关心单次对话内的上下文,用列表保存在内存里就够了。如果希望用户隔天回来,Agent还能记得他的偏好,就需要持久化。
对于这个示例,推荐先用 SQLite 做长期记忆存储。Python 自带sqlite3,不需要额外安装服务,而且足够支撑中小规模应用。表结构可以设计成:
- conversation_id:会话ID。
- user_message:用户输入。
- assistant_message:Agent回答。
- created_at:记录时间。
有人会问:记忆不就是把聊天记录存下来吗?严格说,这只是“存储”,还不是“记忆”。真正的记忆还要做摘要和提取,从历史消息里抽出有价值的用户画像,比如“用户是钢琴初学者”“用户更关心乐器发展史”。示例里先存原始消息,后续在最佳实践部分推荐升级方案。
4.4 搭建Agent主流程
核心流程可以拆成五个步骤:
第一步,接收用户输入。第二步,加载背景知识库,结合用户问题找到相关事实。第三步,从记忆模块读取最近几轮对话和用户画像摘要。第四步,组装System Prompt,把背景知识、记忆摘要、对话历史全部填入。第五步,调用大模型生成回答,并把本轮内容写入记忆。
这里最关键的一步是第四步:组装上下文。上下文的顺序和结构会影响模型理解,建议顺序是“身份设定 → 背景知识 → 记忆摘要 → 最近对话”。
5. 完整示例:实现一个能记住背景的智能体
5.1 背景知识文件
先创建knowledge.json,作为背景知识库:
{ "piano_keyboard_history": { "description": "关于钢琴与键盘发明背景的知识条目", "facts": [ { "id": "fact_001", "keywords": ["钢琴", "发明", "谁", "creator", "发明者"], "question": "谁发明了现代钢琴", "answer": "意大利人巴托罗密欧·克里斯托弗里(Bartolomeo Cristofori)在1700年前后发明了现代钢琴。", "tags": ["钢琴", "发明者", "克里斯托弗里"] }, { "id": "fact_002", "keywords": ["键盘", "前身", "起源", "管风琴", "羽管键琴"], "question": "钢琴键盘的前身是什么", "answer": "钢琴键盘的雏形可以追溯到管风琴和羽管键琴的键盘结构,现代钢琴键盘在此基础上逐渐完善。", "tags": ["钢琴", "键盘", "前身", "管风琴"] } ] } }注意一点:这里的 facts 只作为演示样例,不是严谨的音乐史论文。真实项目里,知识条目需要经过业务负责人确认,并且每一轮更新都要留痕。
5.2 记忆模块
创建memory.py,用 SQLite 保存对话记录:
import sqlite3 import json from datetime import datetime class MemoryStore: def __init__(self, db_path="agent_memory.db"): self.conn = sqlite3.connect(db_path) self._init_table() def _init_table(self): self.conn.execute( """ CREATE TABLE IF NOT EXISTS conversation_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, conversation_id TEXT, role TEXT, content TEXT, created_at TEXT ) """ ) self.conn.commit() def save_message(self, conversation_id: str, role: str, content: str): now = datetime.now().isoformat() self.conn.execute( "INSERT INTO conversation_history (conversation_id, role, content, created_at) VALUES (?, ?, ?, ?)", (conversation_id, role, content, now), ) self.conn.commit() def load_recent_messages(self, conversation_id: str, limit: int = 6): rows = self.conn.execute( """ SELECT role, content FROM conversation_history WHERE conversation_id = ? ORDER BY id DESC LIMIT ? """, (conversation_id, limit), ).fetchall() return list(reversed(rows)) def close(self): self.conn.close()这段代码的作用是把每一轮问答写入本地数据库。load_recent_messages则负责读取最近的对话记录,用于拼接到上下文里。注意:这里用DESC查询再reversed,是为了保证取到的是时间上最新的几条,同时顺序仍然是旧到新。
5.3 Agent核心逻辑
创建piano_agent.py,完成知识检索、上下文组装和模型调用的完整链路:
import json from typing import Optional from memory import MemoryStore class PianoAgent: def __init__(self, knowledge_path: str, memory: Optional[MemoryStore] = None): with open(knowledge_path, "r", encoding="utf-8") as f: self.knowledge = json.load(f) self.memory = memory or MemoryStore() def search_knowledge(self, user_input: str) -> Optional[str]: """在背景知识库中检索与用户输入最相关的一条知识。""" facts = self.knowledge.get("piano_keyboard_history", {}).get("facts", []) for fact in facts: keywords = fact.get("keywords", []) for keyword in keywords: if keyword in user_input: return fact["answer"] return None def build_system_prompt(self, knowledge_answer: Optional[str], recent_messages: list) -> str: """组装 System Prompt,把背景知识和记忆摘要注入上下文。""" system_parts = [ "你是一个专注于音乐史知识问答的智能体。", "回答时必须优先使用背景知识库中的信息;如果背景知识库没有相关内容,请明确说不知道。", ] if knowledge_answer: system_parts.append(f"相关资料:{knowledge_answer}") if recent_messages: memory_lines = [] for role, content in recent_messages: memory_lines.append(f"{role}: {content}") system_parts.append("以下是最近的对话记录:\\n" + "\\n".join(memory_lines)) return "\\n".join(system_parts) def chat_once(self, user_input: str) -> str: # 这里是模型调用入口的占位实现。 # 实际项目中可替换为 openai.ChatCompletion 或任何兼容接口的请求。 raise NotImplementedError("请接入真实的大模型API") def run(self, conversation_id: str, user_input: str) -> str: knowledge_answer = self.search_knowledge(user_input) recent_messages = self.memory.load_recent_messages(conversation_id, limit=4) system_prompt = self.build_system_prompt(knowledge_answer, recent_messages) # 实际项目中,这里应该把 system_prompt 和 user_input 一起发给模型。 # 当前示例为了可运行,返回组合后的 prompt 便于检查。 return system_prompt + "\\n用户问题:" + user_input这个简化版本特意把“模型调用”空出来,目的是先把流程讲清楚。run方法暂时返回组装后的Prompt,方便你验证背景知识有没有被正确注入。接入真实模型时,只需要实现chat_once。
5.4 接入大模型调用
把chat_once替换成真实调用,以 OpenAI 兼容接口为例:
from openai import OpenAI class PianoAgentWithLLM(PianoAgent): def __init__(self, knowledge_path: str, memory: MemoryStore, api_key: str, base_url: str, model: str): super().__init__(knowledge_path, memory) self.client = OpenAI(api_key=api_key, base_url=base_url) self.model = model def chat_once(self, user_input: str) -> str: knowledge_answer = self.search_knowledge(user_input) recent_messages = self.memory.load_recent_messages("default", limit=4) system_prompt = self.build_system_prompt(knowledge_answer, recent_messages) response = self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_input}, ], ) return response.choices[0].message.content再创建一个简单的运行入口main.py:
from memory import MemoryStore from piano_agent import PianoAgentWithLLM def main(): memory = MemoryStore() agent = PianoAgentWithLLM( knowledge_path="knowledge.json", memory=memory, api_key="your-api-key", base_url="your-api-base-url", model="your-model-name", ) conversation_id = "demo_user_001" while True: user_input = input("你:") if user_input.strip().lower() in ("exit", "quit"): break answer = agent.chat_once(user_input) memory.save_message(conversation_id, "user", user_input) memory.save_message(conversation_id, "assistant", answer) print("Agent:", answer) memory.close() if __name__ == "__main__": main()到这里,整个最小可运行的智能体就完成了。
6. 运行结果与效果验证
先验证流程本身,不接大模型也能跑。运行示例中的run方法:
python -c " from memory import MemoryStore from piano_agent import PianoAgent memory = MemoryStore() agent = PianoAgent(knowledge_path='knowledge.json', memory=memory) prompt = agent.run('demo_001', '谁发明了钢琴键?') print(prompt) "预期输出应当包含背景知识库中关于克里斯托弗里的说明,并且能看到最近对话记录为空,说明首次问答时上下文里没有历史记忆。
接入真实模型后,对话效果可以这样判断:
第一轮输入“谁发明了钢琴键”,Agent 应该返回背景知识库里的答案,语气和内容都受 System Prompt 约束。判断标准是答案与knowledge.json中的描述一致,且不会随意扩展出知识库外的细节。
第二轮输入“那键盘的前身呢”,如果记忆模块生效,Agent 能够看到用户第一轮问的“谁发明了钢琴键”,然后在回答“键盘前身”时自然带出管风琴、羽管键琴等知识。如果回答跟第一轮毫无关联,说明上下文记忆没有拼接成功。
如果返回结果不符合预期,第一步不是改提示词,而是检查build_system_prompt生成的最终文本。把拼接后的 Prompt 打印出来,看背景知识有没有被检索到、最近对话有没有丢失,问题基本就能定位。
7. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| Agent 回答内容完全脱离知识库 | 知识检索没有命中 | 打印 search_knowledge 返回结果是否为 None | 扩充 keywords 关键词列表,或用向量检索做语义匹配 |
| 用户问同样的问题,每次答案都不同 | System Prompt 约束不足 | 检查 system prompt 中是否有“必须使用资料”的措辞 | 增加强约束表达,并在提示词中强调权威答案 |
| 第二轮的对话上下文丢失 | 记忆保存顺序异常 | 查看 SQLite 中 conversation_history 表内容 | 修正 save_message 顺序,确认插入时 role 正确 |
| 知识库更新后回答不更新 | 动态加载逻辑缺失 | 确认 JSON 文件在请求时是否重新读取 | 每次请求重新读取知识库,或用数据库存知识 |
| 提示词太长导致请求失败 | 背景知识加记忆超过上下文窗口 | 查看请求日志中的 token 用量 | 精简知识条目,只保留相关内容;历史消息做摘要 |
| 数据库文件被占用无法写入 | 多进程同时写 SQLite | 检查数据库锁错误 | SQLite 适合单机小规模场景;高并发可换 PostgreSQL |
这里重点提醒一下:知识库更新问题最容易忽略。很多人把知识写死在代码里,发版后才发现改不了。正确做法是把知识库做成外部配置,每次请求读取最新数据,或者至少提供缓存刷新机制。
8. 最佳实践与工程建议
8.1 知识库单独管理,不要写死在代码里
上面示例把知识放在knowledge.json里,已经比写死在 Python 代码里前进了一步。真实项目建议用数据库存储,并且提供后台管理页面,让业务人员能够更新知识,而不需要开发改代码发版。
8.2 记忆要做摘要,不能只存原始日志
长期对话之后,直接把所有历史消息塞给模型会超过窗口限制,也不利于模型抓住重点。常见做法是维护一份“用户画像摘要”,每过几轮对话就调用一次模型,把历史消息浓缩成几个标签。下一次请求只传摘要,不传全部日志。
8.3 先跑通最小链路,再上复杂框架
很多人做智能体一上来就接 LangChain、向量数据库、多Agent框架,结果问题太多反而跑不通。更稳妥的路径是:先用 JSON + SQLite 把“知识检索 + 上下文组装”这个最小链路跑通,确认效果稳定之后,再渐进引入向量检索、工作流编排、多Agent协作。
8.4 安全与权限边界
如果这个智能体要接入企业内部系统,背景知识库可能包含敏感数据。务必做到:第一,知识按权限分组,不同用户只能检索到对应范围;第二,对话日志中如果包含个人信息,要加密存储并设置保留期限;第三,模型回答不能绕过权限直接透出数据库内容。最小权限原则同样适用在知识检索层。
8.5 日志与可观测性
智能体出错的排查比普通程序困难,因为中间隔了一次模型调用。建议每个请求都打印完整的 System Prompt、检索命中的知识条目、模型原始输出。只要把这三种日志留全,90% 的问题都能快速定位。
8.6 预留“不知道”出口
知识库没有覆盖到的问题,Agent 应该明确说“根据现有资料无法回答”,而不是硬编一个答案。这既是对用户负责,也是知识库迭代的依据。可以建立一个“未命中问题”日志表,定期分析用户都在问什么,再补充知识条目。
9. 结语与后续学习方向
回到标题里的“记住谁发明了钢琴键背景智能体”。这个例子虽然小,但它把知识型智能体的完整套路走了一遍:背景知识被设计成结构化文件,记忆被存入数据库,对话时两者被组合成上下文,再交给模型生成回答。
如果只是简单让大模型回答历史问题,不需要做这些工程。但一旦你想让Agent稳定服务于某个业务场景,比如产品知识问答、售后自动答复、内部文档助手,“按需注入背景知识 + 跨轮记忆”就是绕不开的两个模块。
顺着这个方向继续深入,建议下一步研究三件事:第一,用向量检索替换关键词匹配,让知识库支持更自然的问法;第二,把 SQLite 升级成独立数据库,并实现知识版本管理;第三,引入多Agent设计,让“记忆管理”“知识检索”“对话生成”变成独立子系统,各自维护、各自扩展。
真正用的时候你会发现,智能体的能力边界不是模型决定的,而是背景知识的设计质量决定的。知识整理得越清楚,Agent 越靠谱。这条经验,比任何提示词技巧都值钱。