knowledge:把 PDF/CSV/文本喂给 Agent 的知识库
D38 的 RAG 是底座;今天在它上面搭"知识库"。knowledge/ 让你把 PDF、CSV、Excel、JSON、纯文本、字符串等静态资料预先灌进向量库,Agent 干活时能按查询检索相关片段。今天读三层:Knowledge(聚合多个知识源 + 一个存储)、BaseKnowledgeSource(各类源的共同基类,负责切块 chunk)、KnowledgeStorage(把 chunk 塞进 D38 的 ChromaDBClient)。还会看 source_type 字符串怎么被解析成具体的源类型。
痛点:怎么让 Agent 用上"我的资料"
knowledge/ 把"各种格式的资料怎么读、怎么切"抽象成 BaseKnowledgeSource,把"存哪、怎么搜"委托给 D38 的 RAG。你只管声明"我有哪些源",切块/嵌入/存/搜全自动。Knowledge:聚合多个源 + 一个存储
Knowledge(knowledge/knowledge.py:88)就是"一堆源 + 一个存储"的组合:
# knowledge/knowledge.py:88(节选)
class Knowledge(BaseModel):
sources: Annotated[list[BaseKnowledgeSource], BeforeValidator(_resolve_knowledge_sources)] = ...
storage: BaseKnowledgeStorage | None = Field(default=None)
embedder: ... = None
collection_name: str | None = None
def __init__(self, collection_name, sources, embedder=None, storage=None, **data):
super().__init__(**data)
if storage is not None:
self.storage = storage
else:
from crewai.knowledge.storage.factory import resolve_knowledge_storage
custom = resolve_knowledge_storage(embedder, collection_name) # 先问工厂
self.storage = custom if custom is not None else KnowledgeStorage(
embedder=embedder, collection_name=collection_name) # 默认
self.sources = sources
def add_sources(self) -> None:
for source in self.sources:
source.storage = self.storage # ★把存储注入每个源
source.add() # ★每个源自己去切块+存
def query(self, query, results_limit=5, score_threshold=0.6) -> list[SearchResult]:
return self.storage.search(query, limit=results_limit, score_threshold=score_threshold)
sources + storage一个 Knowledge 管多个知识源(可以既有 PDF 又有字符串),共用一个存储(一个向量库集合)。storage 工厂兜底没传 storage 就先问全局工厂 resolve_knowledge_storage(可插拔,和 memory 的 factory 同款模式),没命中再建默认 KnowledgeStorage。add_sources 注入 + 触发★把 storage 塞给每个源,再调 source.add()——具体怎么读文件、怎么切块由源自己实现,Knowledge 只负责编排。query 直接转发检索就是转发给 storage.search(D38 的 RAG 检索),默认取 5 条、阈值 0.6。source_type:字典自动解析成具体源类型
sources 字段上挂了个 BeforeValidator,能把字典按 source_type 解析成对应类(knowledge/knowledge.py:23):
# knowledge/knowledge.py:23
_KNOWN_SOURCES: dict[str, type[BaseKnowledgeSource]] = {
"string": StringKnowledgeSource, "docling": CrewDoclingSource,
"csv": CSVKnowledgeSource, "excel": ExcelKnowledgeSource,
"json": JSONKnowledgeSource, "pdf": PDFKnowledgeSource,
"text_file": TextFileKnowledgeSource,
}
# knowledge/knowledge.py:34
def _resolve_knowledge_sources(value: Any) -> Any:
if not isinstance(value, list): return value
resolved = []
for idx, item in enumerate(value):
if isinstance(item, dict):
tag = item.get("source_type")
cls = _KNOWN_SOURCES.get(tag) # 按 source_type 找类
if cls is None:
raise ValueError(f"Unknown source_type={tag!r} at index {idx}: "
f"expected one of {sorted(_KNOWN_SOURCES)}")
resolved.append(cls.model_validate(item)) # 用对应类校验/构造
else:
resolved.append(item) # 已是实例,原样
return resolved
_KNOWN_SOURCES 注册表字符串标签 → 源类的映射表。七种内建源:string/pdf/csv/excel/json/text_file/docling。BeforeValidator 解析让你能用 {"source_type":"pdf","file_paths":[...]} 这种纯字典(比如从 YAML 配置来)声明知识源,框架自动实例化成 PDFKnowledgeSource——配置友好。未知标签报清晰错拼错 source_type 时直接列出所有合法值,不让你猜。实例直接放行如果你传的已经是源实例(不是字典),原样保留——字典和实例两种写法都支持。_KNOWN_SOURCES 注册一行。BaseKnowledgeSource:所有源的公共"切块"逻辑
所有源的基类(knowledge/source/base_knowledge_source.py:16),核心是切块和保存:
# knowledge/source/base_knowledge_source.py:16(节选)
class BaseKnowledgeSource(BaseModel, ABC):
chunk_size: int = 4000 # 每块最多 4000 字符
chunk_overlap: int = 200 # 相邻块重叠 200 字符
chunks: list[str] = Field(default_factory=list)
chunk_embeddings: list[np.ndarray] = Field(default_factory=list, exclude=True)
storage: BaseKnowledgeStorage | None = Field(default=None)
@abstractmethod
def validate_content(self) -> Any: ... # 各源自己实现:怎么读
@abstractmethod
def add(self) -> None: ... # 各源自己实现:读→切→存
def _chunk_text(self, text: str) -> list[str]:
return [text[i : i + self.chunk_size] # ★滑动窗口切块
for i in range(0, len(text), self.chunk_size - self.chunk_overlap)]
def _save_documents(self) -> None:
if self.storage is not None:
self.storage.save(self.chunks) # 交给 KnowledgeStorage
else:
raise ValueError("No storage found to save documents.")
chunk_size=4000 / overlap=200每块 4000 字符,相邻块重叠 200 字符——重叠是为了防止把一句话/一个概念从中间切断,边界处两块都能覆盖到。_chunk_text 滑窗★步长是 chunk_size - chunk_overlap = 3800:切一块 4000、往后跳 3800、再切 4000……于是每两块有 200 重叠。一行列表推导搞定。抽象方法 validate/add基类不知道怎么读 PDF/CSV——把"读"留给子类实现,只提供公共的"切块 + 存"。模板方法模式。_save_documents切好的 chunks 交给注入进来的 storage.save——源不关心存哪,存的事归 KnowledgeStorage。chunk_size - chunk_overlap。如果你手贱把 chunk_overlap 设得 ≥ chunk_size(比如 size=1000、overlap=1000),步长就是 0 甚至负数——range(0, len, 0) 直接 ValueError: range() arg 3 must not be zero,负步长则永远前进不了。源码没显式拦这个,所以务必保证 overlap 明显小于 size(默认 200 < 4000 是安全的)。这是配置类参数常见的"两个值有隐含大小关系"的坑。文件源加载三步:处理路径 → 校验 → 读入
最简单的 StringKnowledgeSource(knowledge/source/string_knowledge_source.py:8):
# knowledge/source/string_knowledge_source.py:8(节选)
class StringKnowledgeSource(BaseKnowledgeSource):
source_type: Literal["string"] = "string"
content: str = Field(...)
def validate_content(self) -> None:
if not isinstance(self.content, str):
raise ValueError("StringKnowledgeSource only accepts string content")
def add(self) -> None:
new_chunks = self._chunk_text(self.content) # 切
self.chunks.extend(new_chunks)
self._save_documents() # 存
文件类源走 BaseFileKnowledgeSource(knowledge/source/base_file_knowledge_source.py:44)三步初始化:
# knowledge/source/base_file_knowledge_source.py:44
def model_post_init(self, _: Any) -> None:
self.safe_file_paths = self._process_file_paths() # ① 规整路径(相对→知识目录下)
self.validate_content() # ② 校验文件存在/是文件
self.content = self.load_content() # ③ 抽象:各源读各自格式
# text_file_knowledge_source.py:12 —— 一个具体实现
def load_content(self) -> dict[Path, str]:
content = {}
for path in self.safe_file_paths:
path = self.convert_to_path(path)
with open(path, "r", encoding="utf-8") as f:
content[path] = f.read()
return content
StringSource 最简直接拿 content 切块存。是理解流程的最小例子:切 → extend → save,三行。_process_file_paths把相对路径转成 knowledge/ 目录下的绝对路径(convert_to_path),还兼容废弃的单数 file_path 字段(发警告引导用 file_paths)。validate_content 检查存在文件不存在直接 FileNotFoundError + 友好提示"把资料放进 knowledge 目录"——早失败、给明路。load_content 抽象PDF 用 pdf 库读、CSV 用 csv 读、txt 直接 f.read()——各源实现自己的读法,读完统一是 dict[Path,str],后续切块一视同仁。KnowledgeStorage:知识库对接 RAG 客户端
KnowledgeStorage(knowledge/storage/knowledge_storage.py:22)是"知识库"和"D38 RAG 客户端"之间的适配器:
# knowledge/storage/knowledge_storage.py:37(节选)
@model_validator(mode="after")
def _init_client(self) -> Self:
if self.embedder:
embedding_function = build_embedder(self.embedder) # D40 的嵌入工厂
config = ChromaDBConfig(embedding_function=cast(..., embedding_function))
self._client = create_client(config) # 造一个 ChromaDBClient
return self
def _get_client(self) -> BaseClient:
return self._client if self._client else get_rag_client() # 实例专属 or 全局共享
def save(self, documents: list[str]) -> None:
if not documents: return
client = self._get_client()
collection_name = f"knowledge_{self.collection_name}" if self.collection_name else "knowledge"
client.get_or_create_collection(collection_name=collection_name)
rag_documents: list[BaseRecord] = [{"content": doc} for doc in documents] # chunk→BaseRecord
client.add_documents(collection_name=collection_name, documents=rag_documents)
_init_client有自定义 embedder 就建一个实例专属的 ChromaDBClient(用这个嵌入模型);否则 _get_client 回退到全局共享 client。collection_name 加前缀★集合名统一加 knowledge_ 前缀(knowledge_crew 等)——和别的用途的集合区分开,避免撞名(类比 memory 的 scope 隔离)。chunk → BaseRecord把每个字符串 chunk 包成最简 {"content": doc}——D38 的 _prepare_documents_for_chromadb 会给它算内容哈希当 doc_id,重灌幂等。search 同款search(:59)多个查询串会 " ".join 成一句再搜,返回 D38 的 SearchResult。异常时记日志返回空列表而不抛——知识检索失败不该让 Agent 崩。save 的 except 里专门判 "dimension mismatch"(:124),把底层向量库的晦涩报错翻译成可操作的指引:"你可能混用了不同嵌入模型,试试 crewai reset-memories -a"。这和 memory 的 EmbeddingDimensionMismatchError(D33/D40)是同一种关怀:向量库最容易踩的坑就是"换了嵌入模型但没重建库",维度对不上。与其把底层库的原始异常直接抛给用户(看不懂),不如识别这个高频错、给出确切的修复命令。好错误信息 = 少一次求助。crew 集成与 embedder 的序列化
crew 用 knowledge_sources 一键建知识库(crew.py:675):
# crew.py:675(节选)
@model_validator(mode="after")
def create_crew_knowledge(self) -> Crew:
if self.knowledge_sources:
if isinstance(self.knowledge_sources, list) and all(
isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources):
self.knowledge = Knowledge(
sources=self.knowledge_sources, embedder=self.embedder,
collection_name="crew")
self.knowledge.add_sources() # ★建 crew 时就把资料灌进去
return self
而 embedder 字段有个巧妙的序列化处理(knowledge/knowledge.py:69):
# knowledge/knowledge.py:69
def _serialize_embedder_spec(value: Any) -> dict[str, Any] | None:
if value is None: return None
if isinstance(value, BaseEmbeddingsProvider): return value.model_dump(mode="json")
if isinstance(value, dict): return value
if isinstance(value, type) and issubclass(value, BaseEmbeddingsProvider):
raise TypeError(f"Cannot checkpoint embedder class ...: " # ★类不能序列化
"build_embedder requires an instance or ProviderSpec dict, not a class.")
raise TypeError(f"Cannot serialize embedder of type {type(value).__name__}: ...")
create_crew_knowledge建 crew 时(model_validator after)若给了 knowledge_sources,就建 Knowledge(collection_name="crew") 并立刻 add_sources 灌库——资料在 kickoff 前就绪。embedder 序列化器knowledge 能进检查点,embedder 得能序列化:None/provider 实例/dict 都能转成 JSON dict;但传"类"(而非实例)会明确报错——因为 build_embedder 要实例或 spec,类没法重建。报错即指引又是"与其让它悄悄坏,不如立刻抛带修复建议的错"——让你把 provider 类换成实例。knowledge_sources(agent 级知识库),crew 级和 agent 级独立,靠不同的 collection_name 分开——和 memory 用 scope 隔离异曲同工,只是这里用"集合名"这个更粗的粒度。取舍 + 今日小结
_chunk_text 是最朴素的定长字符窗口 + 固定重叠,不看句子边界、不看段落。更"聪明"的做法是按句子/段落/语义边界切(如 docling 源就做结构化解析)。为什么基类用最笨的定长?因为它零依赖、对任何文本都适用、结果可预测——4000 字符一块就是一块,不需要分句模型、不会因为解析失败而崩。重叠 200 字符则廉价地缓解了"切断概念"的问题。把"够用的通用切法"放基类当默认,把"高级语义切块"留给专门的源(docling)——简单默认 + 高级可选,不为少数场景给所有源加负担。代价是定长切可能切断句子,但对多数 RAG 检索影响不大(嵌入仍能抓住大意 + 重叠兜底)。👶 小白:知识库和记忆,Agent 实际用的时候有啥不一样?
👨🏫 老师:知识库是只读的参考资料——你灌进去,Agent 检索着用,它不会往里写。记忆是可读可写的经验——Agent 边干边记、还会合并更新。用途上:产品手册、公司制度、领域文档 → 知识库;"上次这个客户偏好 X""这个方案试过不行" → 记忆。检索机制上:知识库是纯 RAG(D38 距离转分 + 阈值),记忆是复合评分 + 自适应 Flow(D35)。一个是"查书",一个是"回忆"。
🧠 今天你应该能回答
- 知识库和记忆有什么本质区别?各用于什么?
- Knowledge 怎么聚合源和存储?add_sources 做了什么?
- source_type 注册表怎么把字典变成具体源?
- 切块的 size/overlap 怎么配合?滑窗步长是多少?
- KnowledgeStorage 怎么对接 D38 的 RAG?集合名为什么加前缀?
- 为什么基类用定长切块而不是语义切块?
✋ 10 分钟动手
P=lib/crewai/src/crewai/knowledge
sed -n '23,63p' $P/knowledge.py # source_type 分派
sed -n '16,62p' $P/source/base_knowledge_source.py # 切块 + 保存
sed -n '105,137p' $P/storage/knowledge_storage.py # save 对接 RAG
python -c "
from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource
s = StringKnowledgeSource(content='CrewAI 是多 Agent 编排框架。'*500, chunk_size=1000, chunk_overlap=100)
print('切成', len(s._chunk_text(s.content)), '块')
"
rag/embeddings/ 的 provider 工厂(十几种嵌入模型怎么统一),和 LanceDBStorage 的存储细节(提交冲突重试、compaction、维度校验)。