1. 项目定位
MaxKB 是一个企业级智能体平台。它把以下能力组合成一个平台:
- 知识库 RAG:文档上传、解析、切分、向量化、关键词索引、混合检索、重排、引用展示。
- 应用发布:简单问答应用和工作流应用都可以发布成在线聊天/API/MCP 工具。
- 工作流编排:前端用 LogicFlow 画节点图,后端用
WorkflowManage执行 DAG。 - Agent 工具调用:AI Chat 节点可以接外部 MCP、自定义工具、工作流工具、已发布应用、Skill 包。
- 多端接入:在线聊天、API 调用、嵌入组件、企业微信/钉钉/飞书/Slack/触发器等。
一个准确的心智模型是:
text
MaxKB = Django 平台后端 + Vue 工作台 + PostgreSQL/pgvector 知识库 + Workflow 图执行器 + LangChain/DeepAgents 工具调用层
它的 "Agent" 主要体现在两个层面:
- 工作流节点级 Agent:每个节点是一个能力单元,节点之间通过上下文传递数据。
- 工具调用型 Agent:AI Chat 节点可以挂 MCP/Tool/Skill/Application,模型在对话中决定是否调用工具。
2. 核心技术栈
2.1 后端
源码入口:
main.pyapps/maxkb/settings/apps/maxkb/urls/apps/chat/apps/application/apps/knowledge/apps/tools/
主要依赖来自 pyproject.toml:
- Web 框架:
Django 5.2、Django REST Framework - 数据库:
PostgreSQL、psycopg、pgvector - 缓存:
django-redis - 异步任务:
Celery、celery-once、django-celery-beat - 调度:
APScheduler - LLM 编排:
langchain、langgraph、deepagents - MCP:
langchain-mcp-adapters - Embedding:
sentence-transformers、各模型供应商适配 - 文档解析:
pypdf、python-docx、openpyxl、xlrd、BeautifulSoup
2.2 前端
源码入口:
ui/src/components/ai-chat/index.vueui/src/api/chat/chat.tsui/src/request/chat/index.tsui/src/views/application/ApplicationSetting.vueui/src/workflow/
主要依赖来自 ui/package.json:
- 框架:
Vue 3、Vite、TypeScript - UI:
Element Plus - 状态:
Pinia - 工作流画布:
@logicflow/core、@logicflow/extension - Markdown/公式/图表:
marked、md-editor-v3、katex、mermaid - 代码编辑:
CodeMirror
2.3 存储与基础设施
- 主业务数据库:PostgreSQL
- 向量存储:PostgreSQL 的
vector类型,配合 pgvector 的余弦距离检索 - 全文检索:PostgreSQL
SearchVectorField+websearch_to_tsquery - 缓存:Redis,主要用于 ChatInfo、调试变量、任务锁
- 异步任务:Celery,用于文档向量化、知识库批量处理、长期记忆抽取等
3. 启动与模块结构
main.py 是后端进程入口,主要做三件事:
- 设置
DJANGO_SETTINGS_MODULE=maxkb.settings。 collectstatic收集前端静态资源。- 执行数据库迁移后启动
web或task服务。
核心业务目录:
text
apps/
application/ 应用、工作流、ChatInfo、长期记忆
chat/ 对外聊天 API、MCP server endpoint
common/ 通用工具、文件解析、chunk、锁、响应转换
knowledge/ 知识库、文档、段落、向量、RAG 检索
models_provider/ 模型供应商适配
tools/ 工具、Skill、工具工作流
ops/ Celery、服务管理
ui/
src/components/ai-chat/ 聊天组件
src/workflow/ 工作流画布、节点配置、校验
src/views/application/ 应用设置、MCP/Tool/Skill 配置
4. RAG 是怎么搭建的
MaxKB 的 RAG 分成两条主链路:
- 入库链路:文件上传 -> 文档解析 -> 段落/问题生成 -> chunk -> embedding -> pgvector/full-text 索引。
- 问答链路:用户问题 -> 可选问题改写 -> 检索知识库 -> 可选 rerank -> 组装 prompt -> LLM 生成。
4.1 RAG 数据模型
核心文件:
apps/knowledge/models/knowledge.py
关键模型:
Knowledge:知识库。Document:文档,属于某个知识库。Paragraph:段落,保存原始内容和chunks。Problem:相关问题/问法。ProblemParagraphMapping:问题与段落的关联。Embedding:向量索引表。Termbase:术语表,用于全文检索分词增强。File:原始文件和图片等附件。
重要字段:
text
Paragraph.content 段落文本
Paragraph.chunks 该段落拆出来的 chunk 列表
Embedding.embedding pgvector 向量字段
Embedding.search_vector PostgreSQL 全文检索字段
Document.status 文档任务状态
Paragraph.status 段落任务状态
Embedding 同时存了两套检索索引:
embedding:用于语义相似度检索。search_vector:用于关键词全文检索。
这就是 MaxKB 支持 embedding、keywords、blend 三种检索模式的基础。
4.2 文档上传与解析
入口文件:
apps/knowledge/views/document.pyapps/knowledge/serializers/document.py
核心类:
DocumentSerializers.SplitDocumentSerializers.CreateDocumentSerializers.Operate.refresh
文档解析的核心流程:
text
用户上传文件
-> DocumentView.Split / BatchCreate
-> DocumentSerializers.Split.parse()
-> file_to_paragraph()
-> 遍历 split_handles
-> 命中文件类型对应解析器
-> 返回段落结构
-> 创建 Document / Paragraph / Problem / ProblemParagraphMapping
-> refresh()
-> 触发 embedding_by_document Celery 任务
file_to_paragraph() 会先把原始文件保存成 File,再通过 split_handles 判断使用哪个解析器。
支持的解析器主要在:
text
apps/common/handle/impl/text/
csv_split_handle.py
doc_split_handle.py
html_split_handle.py
pdf_split_handle.py
text_split_handle.py
xls_split_handle.py
xlsx_split_handle.py
zip_split_handle.py
apps/common/handle/impl/qa/
csv_parse_qa_handle.py
md_parse_qa_handle.py
xls_parse_qa_handle.py
xlsx_parse_qa_handle.py
zip_parse_qa_handle.py
apps/common/handle/impl/table/
csv_parse_table_handle.py
xls_parse_table_handle.py
xlsx_parse_table_handle.py
也就是说,MaxKB 不是只支持普通 txt/pdf,而是区分了普通文档、QA 导入、表格导入、zip 批量导入等多种入库形态。
4.3 切分 paragraph 和 chunk
相关文件:
apps/common/utils/split_model.pyapps/common/chunk/__init__.pyapps/common/chunk/impl/mark_chunk_handle.py
MaxKB 里有两个层次的切分:
- 文档解析阶段:把文件切成
Paragraph。 - 向量化阶段:把
Paragraph再切成更小的chunks。
Paragraph.chunks 是数组字段,保存该段落内部的 chunk。默认 chunk 函数是:
text
text_to_chunk(text, chunk_size=256)
MarkChunkHandle 使用标点和长度规则切分:
text
.{1,chunk_size}[。| |\.|!|;|;|!|\n]
如果文本超出规则无法自然切开,会按最大长度兜底切分。
学习重点:
split_model.py更偏文档结构级切分,比如标题、层级、过滤等。common/chunk更偏 embedding 前的短文本切块。Paragraph.content是可展示、可引用的段落。Paragraph.chunks是为了提升 embedding 检索粒度。
4.4 向量化任务
相关文件:
apps/knowledge/task/embedding.pyapps/common/event/listener_manage.pyapps/knowledge/vector/base_vector.pyapps/knowledge/vector/pg_vector.pyapps/knowledge/serializers/common.py
触发入口:
text
DocumentSerializers.Operate.refresh()
-> embedding_by_document.delay(document_id, embedding_model_id)
Celery 任务:
text
embedding_by_document
embedding_by_document_list
embedding_by_knowledge
delete_embedding_by_document
delete_embedding_by_knowledge
ListenerManagement.embedding_by_document() 做了几件重要的事:
- 用
RedisLock锁住embedding:<document_id>,避免同一文档重复向量化。 - 更新
Document.status为STARTED。 - 分页读取
Paragraph,每 5 条一批。 - 调用
VectorStore.get_embedding_vector().batch_save()。 - 每批处理时更新段落状态。
- 最后调用
create_knowledge_index()创建或刷新知识库索引。
BaseVectorStore.batch_save() 会先执行:
text
chunk_data_list()
-> 如果 source_type 是 PARAGRAPH
-> 使用 Paragraph.chunks
-> 如果没有 chunks,就 text_to_chunk(text)
然后 PGVector._batch_save():
text
texts = normalize_for_embedding(...)
embeddings = embedding.embed_documents(texts)
Embedding.bulk_create(...)
每个 chunk 会变成一条 Embedding 记录,同时写入:
embedding=[float(...)]search_vector=SearchVector(Value(to_ts_vector(...)))
4.5 向量数据库与索引
MaxKB 的向量库不是外部 Milvus/Chroma,而是 PostgreSQL + pgvector。
关键文件:
apps/knowledge/models/knowledge.pyapps/knowledge/vector/pg_vector.pyapps/knowledge/serializers/common.py
VectorField.db_type() 返回:
text
vector
说明它直接使用 pgvector 类型。
create_knowledge_index() 会为知识库创建索引。代码里有一个重要约束:
text
超过 2000 维度不创建索引,因为 pgvector hnsw 索引不支持超过 2000 维度
检索时还有一个细节优化:
text
如果同时查多个知识库,不直接 knowledge_id__in
而是按 knowledge_id 逐个 query,再合并排序
原因是每个知识库可能有自己的 partial HNSW index,单独查可以更容易命中索引。
4.6 检索模式
SQL 文件:
apps/knowledge/sql/embedding_search.sqlapps/knowledge/sql/keywords_search.sqlapps/knowledge/sql/blend_search.sql
三种检索:
text
embedding: pgvector cosine distance
keywords: PostgreSQL full-text ts_rank_cd
blend: vector score + keyword score
embedding_search.sql 核心:
sql
(embedding::vector(%s) <=> %s) AS distance
1 - distance AS comprehensive_score
keywords_search.sql 核心:
sql
ts_rank_cd(embedding.search_vector, websearch_to_tsquery('simple', %s), 32)
blend_search.sql 核心:
sql
1 - vector_distance + ts_rank_cd(...)
问答时的检索节点:
apps/application/flow/step_node/search_knowledge_node/impl/base_search_knowledge_node.py
执行流程:
text
BaseSearchKnowledgeNode.execute()
-> 获取知识库列表
-> 权限过滤 filter_authorized_ids
-> 校验多个知识库 embedding_model_id 是否一致
-> embedding_model.embed_query(question)
-> VectorStore.query(...)
-> 查 Paragraph 明细
-> 计算 directly_return
-> 返回 NodeResult
返回给后续节点的字段:
text
paragraph_list
is_hit_handling_method_list
data
directly_return
question
其中 data 是拼进 prompt 的知识上下文,受 max_paragraph_char_number 限制。
4.7 简单应用的 RAG 问答链路
入口:
apps/chat/serializers/chat.py
简单应用走 pipeline:
text
ChatSerializers.chat()
-> chat_simple()
-> PipelineManage.builder()
-> 可选 BaseResetProblemStep
-> BaseSearchDatasetStep
-> BaseGenerateHumanMessageStep
-> BaseChatStep
含义:
BaseResetProblemStep:问题优化/改写。BaseSearchDatasetStep:查知识库。BaseGenerateHumanMessageStep:组装上下文和用户消息。BaseChatStep:调用模型生成答案,也可以处理 MCP/Tool/Skill。
4.8 工作流应用的 RAG 问答链路
工作流应用不固定走一条 pipeline,而是由用户在画布里组合节点,例如:
text
start-node
-> search-knowledge-node
-> reranker-node
-> ai-chat-node
相关节点:
search_knowledge_nodereranker_nodeai_chat_step_nodedirect_reply_nodecondition_nodeparameter_extraction_node
这条链路更灵活,用户可以在检索前加参数抽取,在检索后加重排,也可以命中高相似段落后直接回复。
5. 智能体/工作流是怎么通信的
MaxKB 的工作流不是多个独立进程或服务互相发消息,而是一个 DAG 图执行器在同一后端进程里调度节点。
核心文件:
apps/application/flow/workflow_manage.pyapps/application/flow/i_step_node.pyapps/application/flow/common.pyapps/application/flow/step_node/
5.1 前端如何定义工作流
前端文件:
ui/src/workflow/index.vueui/src/workflow/common/data.tsui/src/workflow/common/validate.tsui/src/workflow/nodes/
前端用 LogicFlow 创建画布,最终保存的是:
text
nodes: 节点列表
edges: 连线列表
每个节点有:
text
id
type
properties.stepName
properties.node_data
properties.config.fields
properties.config.globalFields
properties.config.chatFields
data.ts 定义每类节点暴露哪些字段。后端 WorkflowManage.init_fields() 会读取这些字段,用于后续 prompt 变量替换。
5.2 后端如何执行工作流
入口:
ChatSerializers.chat_work_flow()
流程:
text
ChatSerializers.chat()
-> 判断 application.type
-> WORK_FLOW
-> WorkflowManage(Workflow.new_instance(work_flow), params, ...)
-> work_flow_manage.run()
WorkflowManage.run():
text
if stream:
run_stream()
else:
run_block()
流式执行:
text
run_stream()
-> run_chain_async()
-> await_result()
-> StreamingHttpResponse
节点执行:
text
run_chain_manage(current_node)
-> run_chain(current_node)
-> current_node.run()
-> NodeResult.write_context()
-> get_next_node_list()
-> 递归执行后续节点
如果一个节点后面有多个分支:
text
ThreadPoolExecutor(max_workers=200)
会并发执行后续分支。
5.3 节点之间如何传数据
核心类:
NodeResultINodeWorkflowManage.contextWorkflowManage.chat_contextnode.context
NodeResult 有两个变量字典:
python
NodeResult(node_variable, workflow_variable)
含义:
node_variable:写入当前节点的node.context。workflow_variable:写入全局workflow.context。
默认写入函数在 i_step_node.py:
text
write_context(step_variable, global_variable, node, workflow)
-> node.context[key] = step_variable[key]
-> workflow.context[key] = global_variable[key]
所以节点通信本质是:
text
上游节点输出
-> NodeResult
-> node.context / workflow.context
-> 下游节点通过 get_reference_field 读取
5.4 变量引用与 Prompt 渲染
关键方法:
WorkflowManage.get_reference_field(node_id, fields)WorkflowManage.get_workflow_content()WorkflowManage.reset_prompt(prompt)WorkflowManage.generate_prompt(prompt)
上下文结构:
text
{
"global": workflow.context,
"chat": workflow.chat_context,
"<node_id>": node.context
}
Prompt 渲染使用 Jinja2:
text
PromptTemplate.from_template(prompt, template_format='jinja2')
这意味着用户在前端节点里配置的变量引用,最终会变成:
text
context.get('<node_id>').get('<field>')
再由模板引擎渲染。
5.5 分支、异常和中断
分支判断:
- 如果
NodeResult.node_variable里有branch_id,说明这是条件/异常分支结果。 WorkflowManage._has_next_node()会根据 edge 的 anchor id 判断走哪条边。
异常处理:
- 节点配置里有
enableException。 - 如果开启异常分支,异常会写入:
text
node.context["exception_message"]
node.context["branch_id"] = "exception"
表单中断:
form-node可以中断工作流。- 当表单未提交时,
NodeResult.is_interrupt_exec()会让流程暂停。 - 用户提交后,可以通过
runtime_node_id和node_data恢复执行。
5.6 流式输出如何从节点回到前端
核心类和方法:
NodeChunkManageNodeChunkWorkflowManage.node_chunk_manageWorkflowManage.await_result()base_to_response.to_stream_chunk_response()
执行过程:
text
节点产生 chunk
-> current_node.node_chunk.add_chunk()
-> NodeChunkManage.pop()
-> await_result() yield chunk
-> StreamingHttpResponse
-> 前端 reader.read()
每个流式 chunk 不只包含文本,还包含节点元数据:
text
node_type
runtime_node_id
node_name
view_type
child_node
node_is_end
real_node_id
reasoning_content
node_status
所以前端可以展示:
- 当前哪个节点在执行。
- 节点是否结束。
- 子应用/循环节点的运行状态。
- 思考内容。
- 工具调用渲染。
6. MCP 和 Skill 是怎么应用的
MaxKB 里有三类容易混淆的概念:
- MCP Server:外部工具服务,或 MaxKB 自己暴露出来的应用工具。
- Tool:平台内的工具记录,可以是自定义 Python 工具、工作流工具、MCP 工具。
- Skill:平台内的一种工具类型,通常是上传的 zip/file 包,在运行时解压到临时目录供 DeepAgents 使用。
它和当前 Codex 的 SKILL.md 技能不是一套东西。
6.1 独立 MCP 节点
文件:
apps/application/flow/step_node/mcp_node/impl/base_mcp_node.py
执行流程:
text
BaseMcpNode.execute()
-> 判断 mcp_source
-> referencing: 从 Tool 表读取 tool.code
-> custom: 使用节点配置 mcp_servers
-> handle_variables() 替换变量
-> validate_mcp_transport()
-> MultiServerMCPClient(servers)
-> client.session(mcp_server)
-> s.call_tool(mcp_tool, params)
-> NodeResult(result, tool_params, mcp_tool)
这个节点适合"显式调用某个 MCP tool"。
6.2 AI Chat 节点里的 MCP/Tool/Skill
文件:
apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.pyapps/application/flow/tools.py
AI Chat 节点更强,它不是固定调用一个工具,而是把工具交给模型,让模型在生成过程中决定是否调用。
BaseChatNode.execute() 做完模型、历史消息、prompt 后,会调用:
text
_handle_mcp_request(...)
这个方法会合并多类工具来源:
text
mcp_servers 自定义 MCP server JSON
mcp_tool_ids 引用的 MCP 工具
tool_ids 自定义工具/工作流工具
application_ids 已发布应用作为工具
skill_tool_ids Skill 工具
最后统一交给:
text
mcp_response_generator(...)
6.3 自定义工具如何转成 MCP
文件:
apps/common/utils/tool_code.py
关键方法:
ToolExecutor.generate_mcp_server_code()ToolExecutor.get_tool_mcp_config()
流程:
text
读取 Tool.code
-> AST 解析 Python 函数
-> 给函数加 @mcp.tool
-> return 包装成 Result(result, tool_id)
-> 生成 FastMCP server 代码
-> gzip + base64 压缩
-> python -c 启动 stdio MCP server
生成的配置类似:
json
{
"command": "python",
"args": ["-c", "..."],
"cwd": "...sandbox...",
"env": {
"LD_PRELOAD": "...sandbox.so"
},
"transport": "stdio"
}
注意:当前代码里 get_tool_mcp_config() 生成的是 transport=stdio,但 validate_mcp_transport() 只允许 sse 和 streamable_http。从源码看,这可能在部分调用路径存在兼容风险,实际是否触发要看对应路径是否对生成后的工具配置再次执行校验。
6.4 应用如何变成 MCP 工具
文件:
apps/common/utils/tool_code.pyapps/chat/views/mcp.pyapps/chat/mcp/tools.py
ToolExecutor.get_app_mcp_config(api_key) 会生成:
json
{
"url": "http://127.0.0.1:8080/<chat_path>/api/mcp",
"transport": "streamable_http",
"headers": {
"Authorization": "Bearer <api_key>"
}
}
MaxKB 自己的 MCP endpoint:
text
apps/chat/views/mcp.py
-> initialize
-> tools/list
-> tools/call
MCPToolHandler.list_tools() 暴露一个工具:
text
agent_<application_id前8位>
MCPToolHandler.call_tool() 内部会:
text
新开 chat_id
-> ChatSerializers.chat(payload)
-> 读取 streaming_content
-> 拼接 operate=True 的 content
-> 去掉 tool_calls_render 标签
-> 返回 MCP tool result
这就是"应用作为工具"的实现。一个 MaxKB 应用可以被另一个 MaxKB 应用通过 MCP 调用。
6.5 Skill 如何管理和运行
Skill 相关路径:
apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.pyapps/application/flow/tools.pyapps/tools/serializers/tool.pyapps/tools/models.pyapps/common/utils/tool_code.py
AI Chat 节点里,Skill 通过 skill_tool_ids 选择。
_handle_mcp_request() 对 Skill 的处理:
text
for tool_id in skill_tool_ids:
-> 查询 Tool
-> 读取 tool.code 作为 file_id
-> 读取 init_params
-> 组装 skill_file_items
-> mcp_servers_config["skills"] = skill_file_items
_initialize_skills() 对 Skill 的处理:
text
mcp_config = json.loads(mcp_servers)
if "skills" in mcp_config:
-> 查询 File
-> file.get_bytes()
-> zip 解压到 /tmp/<chat_id>/skills
-> 校验 zip 内路径,禁止 .. 和绝对路径
-> 把 params 写入每个顶层目录的 .env
-> chmod
client = MultiServerMCPClient(mcp_config)
随后 _yield_mcp_response():
text
create_deep_agent(
model=chat_model,
backend=SandboxShellBackend(root_dir=temp_dir, virtual_mode=True),
skills=["/skills"],
tools=tools,
checkpointer=MemorySaver()
)
这里 Skill 的本质是:
text
上传的技能包文件
-> 运行时解压
-> 作为 deepagents skills 目录
-> 配合 MCP tools 一起给 Agent 使用
6.6 DeepAgents 执行层
文件:
apps/application/flow/tools.py
关键函数:
_yield_mcp_response()mcp_response_generator()
流程:
text
mcp_response_generator()
-> 创建 /tmp/<chat_id>/skills
-> 在全局 asyncio loop 里执行 _yield_mcp_response()
-> Queue 把 async chunk 转同步 generator
-> 外层工作流按普通流式响应消费
_yield_mcp_response():
text
_initialize_skills()
-> MultiServerMCPClient.get_tools()
-> create_deep_agent()
-> agent.astream(..., stream_mode="messages")
-> 解析 AIMessageChunk / ToolMessage / tool_call_chunks
-> yield 内容给前端
这里做了不少兼容处理,例如:
- Qwen/OpenAI 兼容流式 tool_call_chunks。
- 有些模型先发
{}占位,再发真实 JSON fragments。 invalid_tool_calls里也可能有中间 JSON 片段。- 需要把 tool call id 归一化,避免 ToolMessage 找不到对应调用。
这是 MaxKB Agent 工具调用链路里非常复杂、也非常有价值的一段代码。
7. 状态管理
MaxKB 的状态管理按生命周期分层,不是一个单一 store。
7.1 工作流运行时状态
文件:
apps/application/flow/workflow_manage.pyapps/application/flow/i_step_node.py
运行时状态:
text
WorkflowManage.context 全局变量
WorkflowManage.chat_context 会话变量
node.context 节点变量
WorkflowManage.answer 当前回答文本
WorkflowManage.answer_list 分段回答
WorkflowManage.future_list 并发执行 future
NodeChunkManage 流式 chunk 队列
节点输出通过 NodeResult 写入这些上下文。
7.2 聊天会话状态
文件:
apps/application/serializers/common.pyapps/application/models/application_chat.py
核心对象:
ChatInfoChatChatRecord
ChatInfo 是运行时聊天快照,会缓存到 Redis:
text
ChatInfo.set_cache()
ChatInfo.get_cache(chat_id)
timeout = 30 分钟
它保存:
text
chat_id
chat_user_id
chat_user_type
knowledge_id_list
exclude_document_id_list
application_id
chat_record_list
debug
最终持久化:
text
application_chat
application_chat_record
ChatRecord.details 是理解工作流执行细节的关键字段,它保存每个节点的运行详情、输入输出、耗时、状态、错误等。
7.3 会话变量状态
ChatInfo.set_chat_variable():
- 非 debug:写入
Chat.meta - debug:写入 Redis cache
这说明 MaxKB 把"长期会话变量"和"调试态临时变量"分开存储。
变量赋值节点相关:
apps/application/flow/step_node/variable_assign_node/impl/base_variable_assign_node.py
它可以写:
- 全局变量
- chat 变量
- 输出变量
- 循环上下文变量
7.4 文档任务状态
文件:
apps/knowledge/models/knowledge.pyapps/common/event/listener_manage.py
Document.status 和 Paragraph.status 是字符串,但内部用 Status 类按任务类型拆分。
常见任务:
- EMBEDDING:向量化
- GENERATE_PROBLEM:生成相关问题
- SYNC:同步
- TOKENIZE:分词/索引
ListenerManagement.update_status() 会批量更新状态和统计数据。
任务去重:
text
RedisLock("embedding:<document_id>")
celery-once once={"keys": ["document_id"]}
这能避免重复向量化造成资源浪费和脏数据。
7.5 长期记忆状态
文件:
apps/application/long_term_memory/__init__.pyapps/application/models/application_chat.py
模型:
text
ApplicationLongTermMemory
application
chat_user_id
memory
触发位置:
WorkFlowPostHandler.handler()
每轮对话结束后:
text
extract_long_term_memory.apply_async(..., countdown=1)
长期记忆支持:
- 按轮数触发
- daily
- weekly
- monthly
- interval
- cron
抽取逻辑会读取历史 ChatRecord,结合已有 memory,用模型生成结构化记忆,再写回 ApplicationLongTermMemory。
7.6 Agent 工具调用状态
DeepAgents 里使用:
text
MemorySaver()
thread_id = chat_id
这让工具调用过程中 Agent 的中间状态能按 chat_id 维持。与此同时,MaxKB 外层仍然把最终结果落到 ChatRecord.details 和 answer_text_list。
8. 前端、后端、Agent 如何通信
这是从用户点发送按钮到模型返回答案的完整链路。
8.1 前端打开会话
文件:
ui/src/components/ai-chat/index.vueui/src/api/chat/chat.ts
前端先调用:
text
openChatId()
-> getOpenChatAPI()(application_id)
-> 返回 chat_id
聊天组件把 chat_id 存到:
text
chartOpenId.value
8.2 前端发送消息
chatMessage() 构造请求:
json
{
"message": "用户输入",
"stream": true,
"re_chat": false,
"form_data": {},
"image_list": [],
"document_list": [],
"audio_list": [],
"video_list": [],
"other_list": []
}
调用:
text
getChatMessageAPI()(chartOpenId.value, obj)
ui/src/api/chat/chat.ts:
text
postStream(`${prefix}/chat_message/${chat_id}`, data)
ui/src/request/chat/index.ts:
text
fetch(url, {
method: "POST",
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
"AUTHORIZATION": "Bearer <token>",
"Accept-Language": language
}
})
8.3 后端聊天入口
文件:
apps/chat/views/chat.pyapps/chat/serializers/chat.py
入口:
text
ChatView.post(request, chat_id)
-> ChatSerializers(...).chat(request.data)
ChatSerializers.chat():
text
校验 ChatMessageSerializers
-> get_chat_info()
-> get_application()
-> get_chat_user()
-> 校验 chat_id / 权限 / 访问次数
-> SIMPLE: chat_simple()
-> WORK_FLOW: chat_work_flow()
8.4 简单应用通信链路
text
前端 fetch
-> ChatView.post
-> ChatSerializers.chat_simple
-> PipelineManage
-> BaseSearchDatasetStep
-> BaseGenerateHumanMessageStep
-> BaseChatStep
-> LLM / MCP / Tool / Skill
-> StreamingHttpResponse
-> 前端 reader.read()
8.5 工作流应用通信链路
text
前端 fetch
-> ChatView.post
-> ChatSerializers.chat_work_flow
-> WorkflowManage.run()
-> run_stream()
-> run_chain_async()
-> 节点执行
-> NodeResult.write_context()
-> NodeChunkManage
-> StreamingHttpResponse
-> 前端 reader.read()
如果 AI Chat 节点开启工具:
text
AI Chat Node
-> _handle_mcp_request
-> 合并 MCP/Tool/Application/Skill
-> mcp_response_generator
-> create_deep_agent
-> agent.astream
-> tool call / tool result / final answer
-> NodeResult
-> 工作流流式响应
8.6 前端如何消费流式响应
文件:
ui/src/components/ai-chat/index.vue
关键逻辑:
text
const reader = response.body.getReader()
const write = getWrite(chat, reader, ...)
return write()
也就是说,MaxKB 不是用 WebSocket,也不是浏览器 EventSource,而是:
text
fetch POST + response.body.getReader() + text/event-stream 格式
后端 chunk 格式大致是:
json
{
"chat_id": "...",
"id": "...record_id...",
"operate": true,
"content": "...",
"is_end": false
}
工作流模式还会带节点元数据,前端据此更新节点执行状态和回答内容。
9. 复杂点与亮点
9.1 RAG 入库不是简单切文本
亮点:
- 支持普通文档、QA、表格、zip、多格式解析。
- 保留原始
File,方便溯源和图片处理。 - 同时维护
Paragraph.content和Paragraph.chunks。 - embedding 和 full-text 两套索引并存。
- 每知识库 HNSW partial index,检索时按知识库拆开查以利用索引。
复杂点:
- 文件解析器多,格式差异大。
- 文档/段落任务状态是复合字符串,需要理解
Status和TaskType。 - chunk 粒度、段落粒度、引用展示粒度不是同一个东西。
9.2 工作流是平台核心,而不是附加功能
亮点:
- 前端节点配置与后端节点执行高度对应。
- 节点暴露字段可被后续节点引用。
- 支持条件分支、异常分支、循环、表单中断、子应用调用。
- 流式响应包含节点级状态,方便前端做可视化执行过程。
复杂点:
WorkflowManage承担调度、上下文、流式输出、恢复执行、并发分支等多重职责。node.context、workflow.context、chat_context的生命周期不同,新手容易混。- 节点恢复执行依赖
runtime_node_id、ChatRecord.details和前端传回的node_data。
9.3 MCP/Tool/Skill 统一抽象很强
亮点:
- 外部 MCP、自定义 Python 工具、工作流工具、已发布应用、Skill 都能统一进入 Agent 工具集合。
- 自定义工具可动态包装成 FastMCP server。
- 应用可作为 MCP 工具被另一个应用调用。
- Skill zip 解包后交给
deepagents,同时支持.env初始化参数。
复杂点:
- MCP transport 有
sse、streamable_http、stdio多种路径,源码中存在校验与生成配置不完全一致的风险点。 - 流式 tool call 兼容大量模型差异,
tool_call_chunks解析逻辑复杂。 - 工具执行涉及 sandbox、临时目录、权限、压缩代码、环境变量,排障难度高。
9.4 状态分层设计清晰
亮点:
- Redis 缓存
ChatInfo,提升连续对话性能。 - DB 持久化
ChatRecord.details,便于审计和重放。 - 工作流运行时上下文只在执行期间存在,避免污染持久层。
- 长期记忆异步抽取,不阻塞当前回答。
- 文档任务用 RedisLock + celery-once 双重防重复。
复杂点:
- 同一个"状态"可能同时存在运行时、缓存、数据库三个副本。
- 调试态和发布态的变量存储位置不同。
- 长期记忆触发策略较多,需要结合应用配置理解。
9.5 前后端通信选择务实
亮点:
- 用
fetch + ReadableStream实现 POST 流式响应,比 EventSource 更适合带复杂 body。 - 后端统一输出
text/event-stream,前端统一解析。 - 工作流节点状态和最终答案共用同一条流,前端体验连贯。
复杂点:
- 流式内容既有模型文本,又有节点状态、工具调用渲染、思考内容。
- 前端需要处理半包、结束标记、错误状态、滚动、重新生成、表单提交等。
10. 源码阅读路线
建议按以下顺序读,不要一上来钻进所有节点实现。
第一阶段:跑通聊天主链路
ui/src/components/ai-chat/index.vueui/src/api/chat/chat.tsui/src/request/chat/index.tsapps/chat/views/chat.pyapps/chat/serializers/chat.py
目标:理解一次用户消息如何进后端,又如何流式回前端。
第二阶段:理解简单 RAG
apps/application/chat_pipeline/apps/application/chat_pipeline/step/search_dataset_step/apps/application/chat_pipeline/step/chat_step/apps/knowledge/vector/pg_vector.pyapps/knowledge/sql/*.sql
目标:理解简单应用的固定 RAG pipeline。
第三阶段:理解知识库入库
apps/knowledge/views/document.pyapps/knowledge/serializers/document.pyapps/common/handle/impl/text/apps/common/utils/split_model.pyapps/common/chunk/apps/knowledge/task/embedding.pyapps/common/event/listener_manage.py
目标:理解文档如何变成 paragraph、chunk、embedding。
第四阶段:理解工作流执行器
ui/src/workflow/common/data.tsui/src/workflow/common/validate.tsapps/application/flow/workflow_manage.pyapps/application/flow/i_step_node.pyapps/application/flow/step_node/search_knowledge_node/apps/application/flow/step_node/ai_chat_step_node/
目标:理解节点图如何执行,以及节点之间如何通信。
第五阶段:理解 MCP/Tool/Skill
apps/application/flow/step_node/mcp_node/impl/base_mcp_node.pyapps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.pyapps/application/flow/tools.pyapps/common/utils/tool_code.pyapps/chat/views/mcp.pyapps/chat/mcp/tools.pyapps/tools/serializers/tool.py
目标:理解 MaxKB 如何把外部工具、自定义工具、应用和 Skill 统一给 Agent 调用。
第六阶段:理解状态与持久化
apps/application/serializers/common.pyapps/application/models/application_chat.pyapps/application/long_term_memory/__init__.pyapps/common/constants/cache_version.pyapps/common/event/listener_manage.py
目标:理解 chat、workflow、document task、memory 的状态分别存在何处。
11. 推荐学习切入点
如果你是为了学习 RAG:
text
DocumentSerializers.Split
-> file_to_paragraph
-> Paragraph.chunks
-> embedding_by_document
-> PGVector._batch_save
-> PGVector.query
-> BaseSearchKnowledgeNode.execute
如果你是为了学习 Agent 通信:
text
WorkflowManage.run_chain_manage
-> INode.run
-> NodeResult.write_context
-> WorkflowManage.get_reference_field
-> WorkflowManage.generate_prompt
如果你是为了学习 MCP:
text
BaseChatNode._handle_mcp_request
-> ToolExecutor.get_tool_mcp_config
-> ToolExecutor.get_app_mcp_config
-> mcp_response_generator
-> _initialize_skills
-> create_deep_agent
如果你是为了学习前后端流式通信:
text
chatMessage()
-> postStream()
-> ChatView.post()
-> ChatSerializers.chat()
-> WorkflowManage.await_result()
-> response.body.getReader()
12. 一句话总结
MaxKB 的核心价值不只是"有 RAG",而是把 RAG、可视化工作流、工具调用、MCP、Skill、应用发布和多端聊天统一到同一个平台模型里。学习它时不要只看某一个节点,要抓住这条主线:
text
数据进入知识库,能力进入工具系统,用户请求进入 Chat/Workflow,所有中间结果进入上下文,最终通过流式响应回到前端并持久化。