小林Oncall项目介绍
项目中 Agent 范式是什么
当我在看小林飞书云文档 第二张 2.2 的时候,文中说对话 agent 实现是运用了 ReAct 模式,当时对Agent的范式有点模糊了,所以去又复习了一下。
首先,Agent 的范式有两种——ReAct 和 Plan-Execute-RePlan,区别就是前者是一遍思一遍做,后者是先规划,然后执行(Execute),然后根据执行情况判断是否还需要继续执行(RePlan)
在该项目中是二者一起使用,具体的讲,详细的流程是:
-
Plan:规划器进行问题解决方案的步骤规划,分成【步骤1,步骤2,。。。。】
-
Execute: 内置 ReAct 模式
-
思考:调用LLM,判断是否需要调用工具
-
工具调用:MCP工具调用
-
生成:根据工具调用返回的数据发给LLM,完成当前步骤需要的任务
-
-
RePlan:判断是否还有其他步骤
- 如果没有步骤(信息完善),respond 完成响应,具体响应操作:调用LLM将各个步骤结果进行整合响应,
LangChain 和 LangGraph
作为一个java后端的学生,我对python的生态一无所知,即使在学习 Agent的时候总是听到LangChain 和 LangGraph,但是每次看到这两个的知识都是不一样的,没有一个比较明确的自我定义,导致无法对他们做定位。
LangChain:
- 它的特点是封装:检索器,向量数据库接口,大模型调用接口,Prompt拼接等技术进行封装,降低了配置难度,类比java中的spring生态
写到这里,我有一个思考就是:为什么需要LangGraph来实现复杂的RAG流程,为什么LangChain是链式运行的RAG,在我的理解中,LangChain的作用只是帮我封装了一些传统操作,降低了配置难度,并没有考虑
这个问题目前我还理解不了他的答案,但是写几点
-
LangChain 原理是DAG有向无环图,并不是单向链式,缺点无环,无法实现节点自查再回溯到上一节点
-
LangGraph 的出现原因之一就是实现循环与条件分支,可以回溯,成为一张图
好处:
- 格式化输出:不输出零散数据,整体观感更好
- 上下文补充:利用LLM结合问题将回答变成容易理解的语言
- 逻辑连贯:将独立的步骤结构串联成完整分析
- 如果剩余步骤,循环继续进行 Execute 内容
MCP_Servers
总结一下CLS mcp(集成日志服务)的功能
-
记录每次工具调用的情况-- log_tool_call-修饰器
-
时间字符串转时间类型外加小时偏移量-- parse_time_or_default-内部函数
-
时间类型外加分钟偏移量--generate_time_series-内容函数
-
获得当前时间- get_current_timestamp-mcp
-
通过地区名字获得地区代码-get_region_code_by_name-mcp
-
通过名字获得主题信息-get_topic_info_by_name-mcp
-
通过服务名字获得服务信息--search_topic_by_service_name-mcp
-
查询区间时间内某主题的日志-search_log-mcp
总结一下 monitor mcp(CPU和内存检测监控)功能
-
记录每次工具调用的情况-- log_tool_call-修饰器
-
时间字符串转时间类型外加小时偏移量-- parse_time_or_default-内部函数
-
时间类型外加分钟偏移量--generate_time_series-内容函数
-
模拟CUP增长情况并返回CPU使用情况--query_cpu_metrics
-
模拟内存增长情况并返回内存使用情况--query_memory_metrics
感受:
开始的时候,两个mcp都是400多行的代码,感觉十分牛逼,实际上每个函数都是静态数据,实现的功能跟后端的查找没区别,从静态字典里面查找相关元素或者用模拟增长算法去模拟CPU增长,纯纯没用,只不过说的时候感觉功能十分牛逼,实际全是静态数据。
项目启动时的运行逻辑
详细的代码运行逻辑
每当项目启动后,控制台就会输出很多 logo 日志内容,我非常想知道在不进行任何api调用的情况下,单纯就是启动,代码的运行逻辑是什么
# 使用相对于 app 目录的路径:app/../aiops-docs
aiops_docs_dir = Path(__file__).parent.parent / "aiops-docs" #拼凑路径
if aiops_docs_dir.exists() and aiops_docs_dir.is_dir():#判断是否是一个目录
md_files = list(aiops_docs_dir.glob("*.md"))#找到md的文件,然后放入列表中
if md_files:
try:
result = vector_index_service.index_directory(str(aiops_docs_dir))
except Exception as e:
logger.error(f"❌ 知识库索引失败: {e}")
main.py 中内容:
-
拼凑出目录路径 aiops_docs_dir
-
给目录下的所有 md 和 txt 文件加入向量数据库 vector_index_service.index_directory(str(aiops_docs_dir))
vector_index_service.py 中内容:
# 遍历并索引每个文件
for file_path in files:
try:
self.index_single_file(str(file_path))
result.increment_success_count()
logger.info(f"✓ 文件索引成功: {file_path.name}")
except Exception as e:
result.increment_fail_count()
result.add_failed_file(str(file_path), str(e))
logger.error(f"✗ 文件索引失败: {file_path.name}, 错误: {e}")
上面是 index_directory 关键代码,其他都是路径获取,判空等操作,就十分繁琐。
- 简单讲:index_directory 作用——解析目录每一个合法文件,然后单个完成向量数据库存入
我们继续深入函数 index_single_file 关键代码:
try:
# 1. 读取文件内容
content = path.read_text(encoding="utf-8")
logger.info(f"读取文件: {path}, 内容长度: {len(content)} 字符")
# 2. 删除该文件的旧数据(如果存在)
normalized_path = path.as_posix()#路径存储一致性
vector_store_manager.delete_by_source(normalized_path)//1 调用一次外部函数
# 3. 使用新的文档分割器
documents = document_splitter_service.split_document(content, normalized_path)//2调用两次外部函数
logger.info(f"文档分割完成: {file_path} -> {len(documents)} 个分片")
# 4. 添加文档到向量存储
if documents:
vector_store_manager.add_documents(documents)//调用三次外部函数
logger.info(f"文件索引完成: {file_path}, 共 {len(documents)} 个分片")
else:
logger.warning(f"文件内容为空或无法分割: {file_path}")
index_single_file 的函数逻辑:对于一个文档,
-
删除路径一致的旧文档(vector_store_manager.delete_by_source)
-
文档分割(document_splitter_service.split_document)
-
把chunk加入数据库(vector_store_manager.add_documents)
vector_store_manager.delete_by_source 中的内容
核心代码
expr = f'metadata["_source"] == "{file_path}"'#删除表达式
result = collection.delete(expr)
Cllection.delete 是 pyMilvus 向量数据库的内置函数
document_splitter_service.split_document 中的内容
核心代码
# 智能分割文档 (根据文件类型选择分割器)
if file_path.endswith(".md"):
return self.split_markdown(content, file_path)
else:
return self.split_text(content, file_path)
当时看到这个“智能”我就想笑,只是简单的读取路径后缀做一个if判断,然后就额外封装了一个智能分割文档函数
self.split_markdown 和 self.split_text 中的内容
核心代码
# 第一阶段: 按标题分割
md_docs = self.markdown_splitter.split_text(content)
# 第二阶段: 按大小进一步分割
docs_after_split = self.text_splitter.split_documents(md_docs)
# 第三阶段: 合并太小的分片 (< 300字符)
final_docs = self._merge_small_chunks(docs_after_split, min_size=300)
#--------------------------------分割线--------------------------------------------
# 备注,init的内容补充
# Markdown 标题分割器 (只按一级和二级标题分割,减少分片数)
self.markdown_splitter = MarkdownHeaderTextSplitter(
headers_to_split_on=[
("#", "h1"),
("##", "h2"),
# 不再按三级标题分割,避免过度碎片化
],
)
# 递归字符分割器 (用于二次分割,使用更大的chunk_size)
self.text_splitter = RecursiveCharacterTextSplitter(
chunk_size=self.chunk_size * 2, # 加倍chunk_size,减少分片数
chunk_overlap=self.chunk_overlap,
length_function=len,
is_separator_regex=False,
)
主要就三类函数
-
split_text:调用内置函数按标题分,其实就是简单的识别 h
-
split_documents:调用内置函数按照固定大小分,非语义,这里就很扯,就是暴力分割
-
merge_small_chunks:手写合并函数,合并原理就是手动加 "\n" 实现合并
vector_store_manager.add_documents 内容
核心代码
# 为每个文档生成唯一 id(因为 auto_id=False)
ids = [str(uuid.uuid4()) for _ in documents]
# LangChain Milvus 的 add_documents 会自动调用 embedding_function
# 并进行批量处理,性能更好
result_ids = self.vector_store.add_documents(documents, ids=ids)
函数内容:
-
为documents配置id——ids
-
vector_store.add_documents Milvus 数据库内置的添加函数
最后
我们来整理一下逻辑链
-
main.py——得到目录路径,将目录下文件加入数据库——vector_index_service.index_directory
-
vector_index_service.index_directory 作用——解析目录每一个合法文件,然后单个(index_single_file )完成向量数据库存入
-
vector_index_service.index_single_file:
- 删除路径一致的旧文档(vector_store_manager.delete_by_source)
利用内置删除函数(Cllection.delete)按路径删除chunk
-
文档分割(document_splitter_service.split_document)
-
智能文档分割,其实就是判断后缀分成md分割和txt分割
-
document_splitter_service.split_markdown 和 document_splitter_service.split_text,这两个函数的内置实现流程:
-
split_text:调用内置函数按标题分,其实就是简单的识别 h
-
split_documents:调用内置函数按照固定大小分,非语义,这里就很扯,就是暴力分割
-
merge_small_chunks:手写合并函数,合并原理就是手动加 "\n" 实现合并
-
-
-
把chunk加入数据库(vector_store_manager.add_documents)
- vector_store.add_documents Milvus 数据库内置的添加函数
- 以上就是在项目运行之处,自主完成内置文档上传数据库

浙公网安备 33010602011771号