智能体开发(开源项目MaxKB学习) 1先用大白话理解 RAGRAG 检索增强生成你可以把它理解成用户提问后系统先去资料库里找相关内容再把找来的内容交给大模型让模型“看资料后再回答”。为什么要这样做因为大模型本身有两个问题问题 1它不一定知道你的私有知识比如 MaxKB 里你上传了一份 PDF模型原本并不知道。问题 2它可能胡说模型会“看起来很自信地说错话”这就是幻觉。RAG 的作用RAG 的核心作用就是让模型回答时有依据让模型用你自己的知识库减少胡编乱造让知识可更新不用每次重新训练模型2RAG 在 MaxKB 里的真实流程最简版流程用户提问 ↓ 去知识库检索相关段落 ↓ 把段落拼进 Prompt ↓ 调用大模型生成答案 ↓ 流式返回给前端这就是主线 A 的核心。再展开一点就是 5 步第一步上传文档比如你上传了一个 PDF。第二步文档切块系统不会把整本书直接塞给模型而是切成一段一段的小块。第三步向量化每个小块会变成一串数字也就是 embedding。第四步相似度检索用户提问后系统会把问题也转成向量然后去找最相近的段落。第五步拼 Prompt 调模型把“问题 检索到的段落”一起交给大模型生成回答。3先记住的 6 个关键词这几个词是 RAG 的地基① Chunk分块长文档切成小段。为什么要切因为模型上下文长度有限而且小段更容易检索到。语义单一不会因为一个分块所包含的语义信息过多进而导致后续的检索不精确。因为相似度检索是按语义检索的语义越精确那么它检索的知识也也精确。② Embedding向量化把文本变成数字表示。为什么要向量化因为机器不会“理解文字意思”但能算数字之间的相似度。③ Vector DB向量数据库存向量做相似度检索。MaxKB 里对应的是pgvector 这条链路计划里写在 STUDY_PLAN.md:28-33 和 STUDY_PLAN.md:116-130。④ TopK检索出最相关的前 K 条结果。例如 top3 / top5 / top10。⑤ Prompt给模型的提示词。RAG 里通常会把用户问题检索到的知识片段回答规则一起拼成 prompt。⑥ Streaming / SSE模型不是一次性吐完答案而是一边生成一边返回前端。你会看到“字一个个冒出来”的效果。数据库中的表结构Knowledge知识库壳子/主表 └── Document文档 └── Paragraph段落 └── Embedding向量索引1knowledge是“知识库主表”它存的是知识库的元信息比如名称name所属工作空间workspace_id类型type可用范围scope所属文件夹folder_id使用的向量模型embedding_model_id限制项file_size_limit/file_count_limit扩展信息meta它不存正文内容。2document是知识库下的“文档表”一条知识库可以有多个文档document.knowledge_id - knowledge.id文档表一般负责文档名称文档状态文档类型字符数命中策略等3paragraph是文档拆分后的“段落表”一篇文档会被切成多个段落paragraph.document_id - document.idparagraph.knowledge_id - knowledge.id段落表里保存的是段落内容content标题title顺序position命中次数hit_num分块结果chunks4embedding是“向量表”向量表是检索核心通常一条记录对应一个段落或检索源embedding.knowledge_id - knowledge.idembedding.document_id - document.idembedding.paragraph_id - paragraph.id它存的是向量embedding分词索引search_vector来源类型source_type来源 idsource_id也就是说它不是“知识库本体”而是面向检索的索引层。用户真正导入知识的动作不是直接写 Embedding 表而是先写 Document / Paragraph再异步生成 Embedding。这很重要。代码解析def post(self, request: Request, workspace_id: str, knowledge_id: str): return result.success( DocumentSerializers.Create( data{workspace_id: workspace_id, knowledge_id: knowledge_id, user_id: request.user.id}, ).save(request.data) )把它理解成request.data前端真正提交的文档内容workspace_id / knowledge_id / user_id后端补进去的上下文信息DocumentSerializers.Create(...)把这些数据交给序列化器处理.save(request.data)真正执行“创建文档”result.success(...)把结果包装成统一的成功响应返回给前端文档管理和文本拆分是两条入口A. 文档创建入口对应DocumentView.post(...)DocumentSerializers.Create.save(...)它的作用是前端直接提交“文档内容结构”后端落库成Document Paragraph Problem Mapping然后再触发向量化。B. 文本拆分入口对应DocumentView.Split.post(...)DocumentSerializers.Split.parse(...)file_to_paragraph(...)它的作用是前端上传原始文件后端先把文件保存下来再按文件类型进行拆分生成文档结构化数据。前端上传的是文件后端把它拆成 paragraph 结构。def post(self, request: Request, workspace_id: str, knowledge_id: str): split_data {file: request.FILES.getlist(file)} request_data request.data if patterns in request.data and request.data.get(patterns) is not None and len(request.data.get(patterns)) 0: split_data.__setitem__(patterns, request_data.getlist(patterns)) if limit in request.data: split_data.__setitem__(limit, request_data.get(limit)) if with_filter in request.data: split_data.__setitem__(with_filter, request_data.get(with_filter)) return result.success( DocumentSerializers.Split( data{ workspace_id: workspace_id, knowledge_id: knowledge_id, } ).parse(split_data) )这个接口前端传什么这是一个multipart/form-data请求不是普通 JSON。URL 参数来自哪里workspace_idknowledge_idbody 里可能会传什么file一个或多个文件patterns拆分规则limit每段长度限制with_filter是否自动清洗文本拆分的处理流程第一步提取文件split_data {file: request.FILES.getlist(file)}这里request.FILES是 Django 对上传文件的封装。getlist(file)表示支持一次上传多个文件。第二步提取可选参数patterns limit with_filter这些参数会影响后续拆分逻辑比如按标题拆按标点拆按长度限制拆是否清洗内容第三步进入 serializerDocumentSerializers.Split(...).parse(split_data)也就是把“文件 拆分参数”交给业务处理器。DocumentSerializers.Split.parse()做了什么def parse(self, instance): self.is_valid(instanceinstance, raise_exceptionTrue) DocumentSplitRequest(datainstance).is_valid(raise_exceptionTrue) file_list instance.get(file) return reduce( lambda x, y: [*x, *y], [ self.file_to_paragraph( f, instance.get(patterns, None), instance.get(with_filter, None), instance.get(limit, 4096), ) for f in file_list ], [], )3.1 这里的逻辑分三步A. 校验知识库存在self.is_valid(instanceinstance, raise_exceptionTrue)B. 校验上传参数合法DocumentSplitRequest(datainstance).is_valid(raise_exceptionTrue)这会检查file是否存在limit是否可用patterns是否可用with_filter是否可用C. 对每个文件调用file_to_paragraphself.file_to_paragraph(...)也就是一个文件 → 拆成一个或多个文档结构数据最后reduce(...)是把多个文件拆出来的结果合并成一个大列表。file_to_paragraph()是文本拆分的核心4.1 第一步先把原始文件存档raw_file File( idfile_id, file_namefile.name, file_sizefile.size, source_typeFileSourceType.KNOWLEDGE, source_idself.data.get(knowledge_id), ) raw_file.save(file.read()) file.seek(0)这一步的意思是原始上传文件先落到File表保存后文件内容会进数据库大对象/文件存储逻辑source_typeKNOWLEDGE表示它是知识库来源文件source_idknowledge_id表示它属于哪个知识库4.2 第二步根据文件类型选择拆分器for split_handle in split_handles: if split_handle.support(file, get_buffer): result split_handle.handle(...)这里很关键。系统不是一个固定的拆分逻辑而是按文件类型选择不同 handlerHTMLWordPDFExcelCSVZIP默认文本这就是典型的策略模式。4.3 第三步handler 产出结构化文档数据handle(...)的结果一般就是[ { name: xxx, paragraphs: [ {title: ..., content: ..., problem_list: [...]}, ... ], meta: {...} }, ... ]也就是说拆分器不是直接落库而是生成文档结构数据。4.4 第四步给结果加source_file_iditem[source_file_id] file_id这一步非常重要之后文档就能反查到原始上传文件方便下载源文件、追踪来源、导出拆分后得到的数据会去哪拆分后得到的是一组“文档结构数据”接下来通常会进入DocumentSerializers.Create.save(...)或者DocumentSerializers.Batch.batch_save(...)也就是说拆分是加工原料Create / Batch 才是正式入库再看“文档管理”这条链路def post(self, request: Request, workspace_id: str, knowledge_id: str): return result.success( DocumentSerializers.Create( data{workspace_id: workspace_id, knowledge_id: knowledge_id, user_id: request.user.id}, ).save(request.data) )6.1 文档管理入口做什么这个入口不负责拆文件它负责接收前端已经整理好的文档数据创建Document创建Paragraph创建Problem创建ProblemParagraphMapping保存完之后触发 embedding6.2 这一步和“拆分”有什么关系关系是拆分入口输出的结果可以成为创建文档入口的输入。简单说就是原始文件 - 拆分成结构化文档数据 - 保存成 Document/Paragraph/Problem - 异步向量化[前端上传原始文件] | v [Split 接口] | v [按文件类型拆分] | v [生成文档结构数据] | v [Create / Batch 接口] | v [落库 Document / Paragraph / Problem / Mapping] | v [触发 embedding]为什么要把文件先拆成 paragraph再创建 document因为知识库检索不是按“整篇文档”工作的而是按“段落/片段”工作的。这样做有几个好处更适合向量化embedding 一般处理短文本更有效更适合召回问题往往只命中某一小段更适合编辑文档里某段改了不需要整篇重算更适合管理问题每个段落可以挂多个问题怎么判断文件类型是谁决定用哪个 handler在你前面看到的DocumentSerializers.Split.file_to_paragraph()里有这段for split_handle in split_handles: if split_handle.support(file, get_buffer): result split_handle.handle(...) ...也就是说系统不是先猜文件类型再分发而是把所有 handler 挨个试一遍谁的support()返回 True谁就处理这个文件。这就是 handler 选择机制。PDF handler 怎么判断看 apps/common/handle/impl/text/pdf_split_handle.py:514-518def support(self, file, get_buffer): file_name: str file.name.lower() if file_name.endswith(.pdf) or file_name.endswith(.PDF): return True return False解释它很直接文件名后缀是.pdf就认为这是 PDF返回True根据文件后缀判断。PDF 是怎么拆成 paragraph 的3.1 第一步把上传文件写到临时文件with tempfile.NamedTemporaryFile(deleteFalse) as temp_file: for chunk in file.chunks(): temp_file.write(chunk) temp_file_path temp_file.name为什么要这么做因为PDF 处理库PdfReader更习惯读一个真实文件路径或文件对象。所以它先把上传流写到临时文件。3.2 第二步用PdfReader打开 PDFwith open(temp_file_path, rb) as pdf_file: pdf_document PdfReader(pdf_file)这一步把 PDF 解析成可遍历的文档对象。3.3 第三步优先按目录 TOC 拆result self.handle_toc(pdf_document, limit) if result is not None: return {name: file.name, content: result}含义如果这个PDF 自带目录那优先按目录结构拆分成章节。这比纯按页、按字符拆更自然。3.4 第四步如果没目录就尝试按内部链接拆result self.handle_links(pdf_document, pattern_list, with_filter, limit) if result is not None and len(result) 0: return {name: file.name, content: result}含义有些 PDF 虽然没有标准目录但页面里有内部跳转链接。它会把这些 link 当成章节线索。3.5 第五步如果既没目录也没链接就按正文内容拆content self.handle_pdf_content(file, pdf_document) ... split_model SplitModel(...) return {name: file.name, content: split_model.parse(content)}这时它会抽取 PDF 文本根据字体大小把大标题、小标题、正文拼成类 Markdown 文本再交给SplitModel.parse()做最终切块PDF 的handle_toc()怎么拆4.1 先取目录toc PdfSplitHandle.get_toc(doc) if toc is None or len(toc) 0: return None如果没有目录直接返回None让上层走别的策略。4.2 遍历每个目录项for i, entry in enumerate(toc): level, title, start_page entry每个目录项有层级level章节名title起始页start_page4.3 找结束页if i 1 len(toc): end_page toc[i 1][2] - 1 else: end_page len(doc.pages) - 1也就是当前章节从 start_page 到下一个章节前一页。4.4 清洗标题title PdfSplitHandle.handle_chapter_title(title)比如去掉“第X章”“一、”多余符号4.5 按页抽正文for page_num in range(start_page, end_page 1): text PdfSplitHandle.extract_page_text(doc.pages[page_num])把该章节页的文字拼起来。4.6 按 limit 再细分if 0 limit len(chapter_text): split_text smart_split_paragraph(chapter_text, limit) for text in split_text: chapters.append({title: real_chapter_title, content: text}) else: chapters.append({title: real_chapter_title, content: chapter_text})这一步很关键即使一个章节很长也不会整段塞进去而是按limit做智能分段。PDF 最后如果啥都没有会怎样如果目录和内部链接都不行它会退回到“纯文本拆分”。这时关键函数是content self.handle_pdf_content(file, pdf_document) split_model SplitModel(...) return {name: file.name, content: split_model.parse(content)}handle_pdf_content()是怎么把 PDF 变成“可拆文本”的看 apps/common/handle/impl/text/pdf_split_handle.py:99-146它做了两件事第一步收集所有字体大小for page in pdf_document.pages: lines PdfSplitHandle.extract_page_lines(page)它会把每页的文字和字体大小提取出来。第二步根据字体大小推断标题层级if size_diff 2: content f## {text}\n\n elif size_diff 0.5: content f### {text}\n\n else: content f{text}\n这很像什么像是在把 PDF “翻译成 Markdown”字体明显更大 → 二级标题##字体稍大 → 三级标题###普通字号 → 正文所以 PDF 的核心策略是先提取结构再转成类似 Markdown 的文本再统一交给分段器Markdown / 普通文本是怎么拆的这部分更像你熟悉的“规则切块”。support 判断看 apps/common/handle/impl/text/text_split_handle.py:33-47Markdown.md/.txt直接支持纯文本如果后缀不像图片/视频/音频/PDF再用编码检测判断是不是文本8.2 handle 里的处理流程看 apps/common/handle/impl/text/text_split_handle.py:49-64buffer get_buffer(file) content buffer.decode(detect(buffer)[encoding]) return {name: file.name, content: split_model.parse(content)}解释先拿到文件 bytes检测编码decode 成字符串交给SplitModel.parse()SplitModel.parse()才是真正的“切 paragraph”核心看 apps/common/utils/split_model.py:367-428它的逻辑可以理解成 3 层9.1 先找标题level_content_list parse_title_level(text, self.content_level_pattern, index)比如 Markdown 的标题######这些会被识别为标题层级。9.2 如果根本没有标题就按长度切if len(level_content_list) 0: return [to_tree_obj(row, block) for row in smart_split_paragraph(text, limitself.limit)]也就是说没有标题的文本就按limit智能分段。9.3 如果有标题就构造树再扁平化成段落它会递归标题节点子标题节点block 段落节点最后再转成 paragraph 结构result result_tree_to_paragraph(result_tree, [], [], self.with_filter)最终输出大概长这样[ { title: 第一章 概述, content: ...... }, { title: 第二章 安装, content: ...... } ]最终实现的文本划分文本拆分不是简单按长度截断而是先通过标题正则、目录和章节层级把原始文本构造成树再将树拍平成带标题链的段落对象。最终每个 paragraph 都包含父级标题路径和正文内容既保留结构信息又方便后续 embedding 和检索。对于没有明显标题结构的文本会退化为基于长度和标点的智能分段。在limit限制的500token中找到最后一个。等有明显语义分段的句子划分为一个章节检索链路用户提问后怎么召回相关段落用户问题 - 转向量 - 去 embedding 表检索 - 找到 paragraph_id - 查回 paragraph 原文 - 返回 paragraph_listMCP 是什么MCP 通常可以理解成一种让大模型以统一方式发现、描述和调用外部工具/能力的协议层。你可以把它想成大模型不会直接懂你项目里的 Python 函数它也不会直接懂数据库里的工具记录它只懂“工具描述 参数 schema 调用方式”而 MCP server 就是负责提供这些东西的“工具服务端”。为什么需要 MCP server因为平台里可能有很多不同来源的能力自定义工具数据库里的普通工具另一个子应用skill外部 MCP 工具服务如果每种能力都单独接一次模型会非常乱。所以项目做了一件事把不同来源的能力统一包装成 MCP server 配置交给同一个调用器处理。这就是你看到mcp_servers_config的原因。先看“转换过程”整体框架整体逻辑是1. 准备 mcp_servers_config 空字典 2. 读取自定义 MCP 配置 3. 读取数据库里的 MCP 工具 4. 把普通工具转成 MCP 配置 5. 把子应用转成 MCP 配置 6. 把 skill 转成 MCP 配置 7. 统一交给 mcp_response_generator下面我按代码逐段解释。初始化容器base_chat_node.py:312mcp_servers_config {}解释这是一个空字典后面所有MCP 工具普通工具子应用skill都会往这里塞。它就是最终要交给模型的“工具总配置”。如果是自定义 MCP serverbase_chat_node.py:322-324if mcp_source custom and mcp_servers: mcp_servers_config json.loads(mcp_servers) mcp_servers_config self.handle_variables(mcp_servers_config)逐行解释第 1 行if mcp_source custom and mcp_servers:表示如果当前节点配置的是“自定义 MCP 来源”并且有配置内容。第 2 行mcp_servers_config json.loads(mcp_servers)把 JSON 字符串转成 Python dict。第 3 行mcp_servers_config self.handle_variables(mcp_servers_config)把里面的变量占位符替换成真实值。结果如果用户手工配置了一份 MCP server 配置就直接用它。如果是数据库里的 MCP 工具base_chat_node.py:325-330elif mcp_tool_ids: mcp_tools QuerySet(Tool).filter(id__inmcp_tool_ids).values() for mcp_tool in mcp_tools: if mcp_tool and mcp_tool[is_active]: mcp_servers_config {**mcp_servers_config, **json.loads(mcp_tool[code])} mcp_servers_config self.handle_variables(mcp_servers_config)逐行解释第 1 行elif mcp_tool_ids:如果没用自定义 MCP而是配置了一批 MCP 工具 ID。第 2 行mcp_tools QuerySet(Tool).filter(id__inmcp_tool_ids).values()从数据库Tool表里查出这些工具。第 3 行for mcp_tool in mcp_tools:遍历每个工具。第 4 行if mcp_tool and mcp_tool[is_active]:只处理启用状态的工具。第 5 行mcp_servers_config {**mcp_servers_config, **json.loads(mcp_tool[code])}把工具表里存的code字段解析成 JSON并合并进mcp_servers_config。第 6 行mcp_servers_config self.handle_variables(mcp_servers_config)继续做变量替换。结果数据库里的 MCP 工具被转换成统一 MCP 配置。把普通工具转成 MCP 配置base_chat_node.py:334-356tool_init_params {} tools get_tools( self.workflow_manage.get_source_type(), self.workflow_manage.get_source_id(), tool_ids, workspace_id ) if tool_ids and len(tool_ids) 0: ... custom_tools_map { str(t.id): t for t in QuerySet(Tool).filter(id__intool_ids, tool_typeToolType.CUSTOM, is_activeTrue) } for tool_id in tool_ids: tool custom_tools_map.get(str(tool_id)) if tool is None: continue executor ToolExecutor() init_params_default_value {i[field]: i.get(default_value) for i in tool.init_field_list} if tool.init_params is not None: tool_init_params init_params_default_value | json.loads(rsa_long_decrypt(tool.init_params)) else: tool_init_params init_params_default_value tool_config executor.get_tool_mcp_config(tool, tool_init_params) mcp_servers_config[str(tool.id)] tool_config逐段解释tools get_tools(...)这一步会取平台里“真正可执行的工具对象”。也就是说这里已经不只是配置了还有真实工具实例。custom_tools_map {...}从数据库里把tool_ids对应的自定义工具查出来做成 id - tool 的映射。init_params_default_value ...从工具定义里取初始化字段的默认值。tool.init_params如果工具有加密保存的初始化参数就解密出来并和默认参数合并。tool_config executor.get_tool_mcp_config(tool, tool_init_params)这一步非常关键。它的作用是把平台内部普通工具转换成 MCP 风格的工具配置。也就是说Tool 在数据库里的内部结构模型不认识。但经过get_tool_mcp_config(...)后它就变成模型能理解的工具描述。mcp_servers_config[str(tool.id)] tool_config把转换好的 MCP 配置塞进统一配置字典。结果普通工具完成了项目内部 Tool - MCP 风格配置把子应用转成 MCP 配置base_chat_node.py:358-390if application_ids and len(application_ids) 0: ... for application_id in application_ids: app apps_map.get(str(application_id)) ... executor ToolExecutor() app_config executor.get_app_mcp_config(api_key) mcp_servers_config[app.name] app_config解释这里把一个“应用”也转换成工具。也就是说模型可以把另一个应用当作工具调用。为什么这么设计因为从模型视角看一个工具一个子应用一个远程 MCP 服务本质上都是“我给你输入你返回结果”所以都可以统一为 MCP 配置。创建 deep agentagent create_deep_agent( modelchat_model, backendSandboxShellBackend(root_dirtemp_dir, virtual_modeTrue), skills[/skills], toolstools, system_promptsystem_prompt, interrupt_on{write_file: False, read_file: False, edit_file: False}, checkpointercheckpointer, )这几行是最关键的这一步就是把普通 chat_model 升级成一个具备工具、skill、sandbox 环境的 agent。逐项解释modelchat_model用哪个大模型作为 agent 的大脑。backendSandboxShellBackend(...)给 agent 一个受控执行环境。skills[/skills]让 agent 可以使用 skills 目录里的能力。toolstools把前面整理好的工具列表真正注入 agent。system_promptsystem_prompt给 agent 系统提示词。checkpointercheckpointer支持中间状态保存。这就是 Function Calling 真正落地的地方普通模型chat_model.invoke(messages)带工具模型create_deep_agent(... toolstools ...)也就是说这里把模型从“纯聊天模型”变成了“会调用工具的 agent”。开始流式执行 agentresponse agent.astream( {messages: message_list}, config{recursion_limit: recursion_limit, configurable: {thread_id: chat_id}}, stream_modemessages, )含义现在 agent 开始运行了。注意这里不是直接chat_model.stream(message_list)了而是agent.astream(...)说明接下来模型的输出不只是普通文本 chunk还可能包含tool call chunkinvalid tool call chunktool call metadata模型是怎么发起工具调用的看 tools.py:541-679这部分代码的核心作用是从模型流式输出中识别出“工具调用请求”。1读取tool_call_chunksfor tc_chunk in chunk[0].tool_call_chunks or []: raw_id tc_chunk.get(id) key _get_fragment_key(tc_chunk.get(index), raw_id) _upsert_fragment(key, raw_id, tc_chunk.get(name), tc_chunk.get(args, ))含义如果模型输出的 chunk 里包含工具调用片段就把它收集起来。也就是说模型在流式输出中可能会说我要调用 tool_x 参数是 {city: 上海}这些信息不会直接变成自然语言而是进入tool_call_chunks。2兼容不同模型的工具调用格式后面还有tool_callsinvalid_tool_callsadditional_kwargs[tool_calls]这些逻辑是在做兼容不同模型厂商的 tool call 输出格式不完全一样这里统一做聚合。3检测工具调用结束is_finish_chunk ( chunk[0].response_metadata.get(finish_reason) tool_calls or chunk[0].chunk_position last )含义如果本轮输出是一个工具调用结束块就准备把工具调用信息整理出来。4解析工具参数生成tool_calls_infoparsed_args json.loads(entry[arguments]) filtered_args ... info {name: entry[name], input: json.dumps(filtered_args, ensure_asciiFalse)} tool_calls_info[entry[id]] info含义把模型输出的工具调用请求解析成调哪个工具参数是什么这就是 Function Calling 的结构化调用信息。Function Calling 不是简单手写 if-else 调函数而是通过将 MCP server、自定义工具、子应用和 skill 统一转换成 MCP 风格配置并注入到create_deep_agent(..., toolstools, ...)构造的 agent 运行环境中。模型在流式执行过程中会输出结构化的tool_call_chunks/tool_calls系统再解析这些调用请求并由 agent runtime 执行工具最后把工具结果回灌给模型生成最终答案。这说明项目已经具备了比较完整的 Agent Tool Calling 能力。MCP 解决的是“工具怎么接入”比如在项目里普通 Tool子应用skill自定义 MCP server最后都被转换成mcp_servers_config相关代码在apps/application/flow/step_node/ai_chat_step_node/impl/base_chat_node.py:312-437这说明MCP 更像一个“统一工具注册层 / 工具接入层”。Function Calling 解决的是“模型怎么使用工具”真正进入模型侧的是apps/application/flow/tools.py:467-481agent create_deep_agent( modelchat_model, toolstools, ... ) response agent.astream( {messages: message_list}, ... )这一步开始模型就具备看见工具决定调用工具输出工具调用请求这就是 Function Calling。MCP 不等于 Function Calling如果只有 MCP你只是把工具配置好了但模型未必真的调用它如果只有 Function Calling 概念但没有 MCP 这样的统一层模型能调用工具但工具来源五花八门很难统一接入所以两者不是同义词。用户提问后系统先进入聊天接口恢复会话和应用配置然后在 SIMPLE 模式下通过固定 Pipeline 执行知识库检索、Prompt 组装和大模型回答三个步骤先把问题编码成向量并在embedding表中召回相关段落再把段落拼接到 Prompt 模板中最后调用 chat model 生成答案并返回给前端同时保存会话记录和检索详情。MCP 负责把工具准备好Function Calling 负责让模型在运行时选择并调用这些工具。skill -creator固定流程创建skill,find-skill 查找需要使用哪些skill软件开发Superpowers让ai开发流程标准化superpowers-zh(中文版)GStack全能专家团队使用流程1、确定需求排查隐患测试验证提交代码前端设计frontend-design前端设计ui-ux-pro-maxui-ux-pro-max-skill-cn前端开发skillmattpocock-skills-zh-CN