跳转到内容

架构

标签「架构」下的 3 篇文章

聊聊我对 MCP 的一点理解

全称模型上下文协议(Model Context Protocol)。这是由 Anthropic 推出的一项开放标准,目标是为大型语言模型和 AI 助手提供一个统一、标准化的接口,使 AI 能够轻松操作外部工具并完成更复杂的任务。

通过使用 MCP,Claude 或 ChatGPT 等 AI 应用程序可以连接到数据源(如本地文件、数据库)、工具(如搜索引擎、计算器)和工作流(如专门的提示词),从而使它们能够获取关键信息并执行任务。 可以将 MCP 想象成 AI 应用程序的 USB-C 接口。正如 USB-C 为连接电子设备提供了一种标准化方式一样,MCP 也为连接 AI 应用程序和外部系统提供了一种标准化方式。

就拿我开发的 AI LocalBase 来说吧。AI LocalBase 是一个本地化的知识库和数据管理工具,它可以帮助用户在本地存储和管理各种数据,并通过 AI 助手进行智能查询和操作。 虽然是本地化,但它也可以支持 OpenAPI 和 MCP 协议。通过 MCP 协议,AI LocalBase 可以和 Claude、Cursor、Codex 这类 AI 应用连接起来,使用户能够通过自然语言与本地知识库进行交互。

MCP 协议连接层架构示意图

放到 AI LocalBase 这个项目里看,MCP 其实没有那么玄。

本质上,它还是启动一个后端服务,然后暴露一个符合 MCP 协议的接口,给其他 AI 应用看。

只不过这个接口不是普通业务接口。

普通 HTTP API 更多是给前端页面、脚本或者其他服务调用的。调用方需要提前知道接口路径、参数格式和业务含义。

MCP 接口面对的是 AI 应用。它要让对方能够先发现能力,再决定怎么调用。

比如 AI LocalBase 后端启动后,可以暴露一个 MCP 入口。Claude、Cursor、Codex 这类工具作为 MCP Client 连进来之后,先问后端:

你有哪些工具?

你有哪些资源?

你支持哪些操作?

后端再把自己的能力按 MCP 的方式描述出去。

例如:

  • 可以列出知识库
  • 可以检索某个知识库
  • 可以读取文档片段
  • 可以生成带来源的回答
  • 可以查看索引状态
  • 在有权限时,也可以上传文档或重建索引

这样一来,AI 应用访问的就不是 AI LocalBase 的网页,而是 AI LocalBase 暴露出来的一组能力。

架构上大概就是上面这张图表达的链路:AI 应用通过 MCP Client 连到 AI LocalBase 后端暴露的 MCP 入口,再由后端去访问知识库、文档、向量索引和检索服务。

这个时候,AI LocalBase 就不只是一个可以打开的本地知识库工具了。

它还变成了其他 AI 应用可以调用的知识库后端。

这也是我理解 MCP 的一个关键点:它不是替代业务系统,而是让业务系统把自己的能力用一种标准方式暴露出去。

真正有价值的地方,不是“我又多写了一个接口”,而是这个接口背后有一套能力发现、工具描述、权限控制和调用返回的协议约定。

所以 MCP Server 不是把所有内部接口原样丢给 AI。

更好的做法是站在 AI 应用的视角重新整理能力。

比如对外暴露 search_knowledge_base,而不是暴露一堆底层查询接口。

比如返回检索结果时,不只返回文本,还要返回来源、置信度、文档 ID、chunk 信息,方便 AI 继续组织答案。

比如写入、删除、重建索引这种操作,就不能和普通查询放在同一个权限层级里。

说白了,MCP 的工程价值在于:让后端服务多了一层面向 AI 应用的能力出口。

如果用代码表达,它其实可以很朴素。

下面这些不是完整实现,更像是把核心链路拆开看:后端启动时挂一个 /mcp 入口,然后在这个入口里处理 MCP 的初始化、工具发现和工具调用。

比如在 Go + Gin 后端里,可以大概这样挂路由:

func RegisterRoutes(r *gin.Engine, cfg Config, deps Dependencies) {
api := r.Group("/api")
if cfg.MCP.Enabled {
mcp := NewMCPHandler(deps.KnowledgeBaseService, deps.SearchService)
api.POST(
"/mcp",
RequireAPIKey(),
RequireScope("mcp:read"),
mcp.Handle,
)
}
}

这里的重点不是 /api/mcp 这个路径本身。

重点是:AI LocalBase 后端启动以后,多暴露了一个 MCP 协议入口。其他 AI 应用不是直接访问页面,而是通过这个入口发现和调用后端能力。

MCP 请求底层可以理解成一类 JSON-RPC 消息。后端收到请求以后,根据 method 分发到不同处理逻辑。

type MCPRequest struct {
JSONRPC string `json:"jsonrpc"`
ID any `json:"id,omitempty"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
func (h *MCPHandler) Handle(c *gin.Context) {
var req MCPRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, rpcError(req.ID, -32700, "invalid json"))
return
}
switch req.Method {
case "initialize":
c.JSON(http.StatusOK, h.initialize(req.ID))
case "tools/list":
c.JSON(http.StatusOK, h.listTools(req.ID))
case "tools/call":
c.JSON(http.StatusOK, h.callTool(c.Request.Context(), req.ID, req.Params))
default:
c.JSON(http.StatusOK, rpcError(req.ID, -32601, "method not found"))
}
}

tools/list 可以理解成 AI 应用进来以后先问一句:你这里有什么能力?

后端返回的不是普通菜单,而是一组工具描述。

func (h *MCPHandler) listTools(id any) MCPResponse {
return MCPResponse{
JSONRPC: "2.0",
ID: id,
Result: map[string]any{
"tools": []map[string]any{
{
"name": "list_knowledge_bases",
"description": "列出当前用户可以访问的知识库",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{},
},
},
{
"name": "search_knowledge_base",
"description": "在指定知识库中检索相关文档片段",
"inputSchema": map[string]any{
"type": "object",
"properties": map[string]any{
"knowledgeBaseId": map[string]string{"type": "string"},
"query": map[string]string{"type": "string"},
"topK": map[string]any{"type": "integer", "default": 5},
},
"required": []string{"knowledgeBaseId", "query"},
},
},
},
},
}
}

当 AI 应用决定调用 search_knowledge_base 时,后端再把这个工具调用转成内部业务服务调用。

func (h *MCPHandler) callSearchKnowledgeBase(
ctx context.Context,
args SearchKnowledgeBaseArgs,
) (MCPToolResult, error) {
hits, err := h.search.Search(ctx, SearchRequest{
KnowledgeBaseID: args.KnowledgeBaseID,
Query: args.Query,
TopK: args.TopK,
})
if err != nil {
return MCPToolResult{}, err
}
return MCPToolResult{
Content: []MCPContent{
{
Type: "text",
Text: formatSearchSummary(args.Query, hits),
},
},
Structured: map[string]any{
"query": args.Query,
"hits": hits,
},
}, nil
}

这里其实就能看出 MCP Server 的位置了。

它不是向量数据库。

它也不是 RAG 本身。

它只是把 AI LocalBase 已经有的检索能力,包装成 AI 应用能发现、能理解、能调用的工具。

客户端配置也可以很简单。比如一个支持 HTTP MCP 的 AI 应用,可能只需要知道 MCP 服务地址和访问凭证:

{
"mcpServers": {
"ai-localbase": {
"type": "http",
"url": "http://localhost:8080/api/mcp",
"headers": {
"Authorization": "Bearer lb_xxx"
}
}
}
}

这样配置之后,AI 应用就可以把 AI LocalBase 当成一个外部能力源。

用户问问题时,AI 应用可以先通过 MCP 调用 search_knowledge_base,拿到相关文档片段,再基于这些片段组织回答。

所以从代码角度看,MCP 并不是多神秘的东西。

它就是一层协议适配。

只不过这层适配面向的是 AI 应用,所以它不只要能调用,还要能描述能力、限制权限、返回结构化结果,并且让后续的 Agent 工作流继续往下走。

软考高级架构师考试后的一个感受

昨天,也就是 2026 年 5 月 23 日,去参加了软考高级系统架构设计师考试。

考完之后最明显的感受是:这类考试真的在越来越快地贴近新的技术趋势。以前提到架构,更多想到的是分层、缓存、消息队列、高并发、数据库、微服务这些传统工程问题;但这次做题时,AI、模型、多模态这些内容已经很自然地出现在题目里了。

这种感觉还挺直接的。

不是那种“AI 作为热点,被强行塞进试卷”的感觉,而是它开始变成架构师应该了解的背景知识之一。

上午选择题做下来,感觉整体还可以。

时间上比较充裕,没有那种一路卡住、最后疯狂赶题的感觉。很多题还是围绕软件工程、架构设计、数据库、网络、安全、项目管理这些基础内容展开,只要平时有积累,大多数题都能比较顺地往下做。

比较有意思的是,题型很快就来到了 AI 和模型相关内容。

印象里有一道和 Transformer 相关的题,也有一道涉及多模态的题。看到这些题的时候,会明显感觉到考试范围已经不只是传统软件架构知识了。

这其实也合理。

现在很多系统已经不只是“业务系统 + 数据库 + 缓存 + 接口”这么简单。越来越多项目会接入大模型能力,可能涉及文本生成、向量检索、多模态理解、智能测试、智能客服、知识库问答等场景。

架构师如果完全不了解这些内容,后面做系统设计时确实会越来越吃力。

案例题对我来说还是有一些难。

选择题更多是知识点识别和判断,案例题则更像是把知识放进一个具体场景里,让你分析系统问题、补全架构设计、选择方案、说明理由。

这个部分很考验表达能力,也考验对架构方法的熟练程度。

有时候不是完全不知道,而是知道一些点,但要在有限时间里组织成比较完整、规范、有条理的答案,并不容易。

这也提醒我,备考不能只停留在“看过概念”。案例题需要练的是:

  • 能不能看懂业务场景。
  • 能不能识别系统中的关键矛盾。
  • 能不能把架构方案和问题对应起来。
  • 能不能用比较规范的语言写出答案。

这和实际做架构也很像。真实工作里,知道某个技术名词没有太大意义,关键还是能不能把它放到合适的系统问题里。

这次考试让我比较在意的一点,是 AI 相关内容的出现频率。

选择题里出现了 Transformer、多模态,论文题里也直接出现了“向量数据库”和“多模态大模型在移动智能测试框架中的应用”。

这说明考试已经不只是把 AI 当成一个新名词,而是在尝试把它放进架构设计语境里。

比如向量数据库不是单独存在的知识点,它背后对应的是:

  • 文本向量化。
  • 相似度检索。
  • RAG 检索增强生成。
  • 知识库问答。
  • 语义搜索。
  • 大模型应用的数据底座。

多模态大模型也不是简单知道“能处理图片和文本”就够了。它进入移动智能测试框架时,可能会涉及:

  • UI 截图理解。
  • 测试步骤生成。
  • 异常页面识别。
  • 测试用例自动补全。
  • 文本、图像、操作行为的联合分析。

这些东西已经开始和软件工程、测试框架、系统架构结合起来了。

论文题:四个方向都挺有代表性

Section titled “论文题:四个方向都挺有代表性”

这次论文题是四选一,题目大概是:

  1. 六边形架构设计。
  2. 向量数据库。
  3. 论高并发系统设计。
  4. 论多模态大模型在移动智能测试框架中的应用。

这四个题其实很有代表性。

六边形架构偏架构思想,重点是领域逻辑和外部依赖的隔离。

高并发系统设计是传统架构高频题,缓存、限流、削峰、异步、分库分表、读写分离、降级熔断这些内容都能展开。

向量数据库和多模态大模型则明显代表新趋势,考察的是架构师能不能把 AI 相关能力纳入系统设计。

如果从稳妥角度看,高并发系统设计可能是很多人比较熟悉的方向。它素材多、案例多,也比较容易结合实际项目经验展开。

但从趋势角度看,向量数据库和多模态大模型这两个题很值得重视。

它们释放了一个信号:以后软考高级架构师可能会越来越多地考察 AI 时代下的软件架构能力。

这次考完,最大的感受不是某一道题难不难,而是知识体系真的需要更新。

传统架构能力还是基础。数据库、缓存、消息队列、微服务、高并发、安全、可用性、可扩展性,这些东西不会过时。

但只靠这些已经不够了。

现在还要补上大模型相关的工程知识:

  • Transformer 的基本概念。
  • 向量数据库和语义检索。
  • RAG 应用架构。
  • 多模态模型的输入输出方式。
  • AI 能力如何接入现有业务系统。
  • 模型服务的成本、延迟、稳定性和安全边界。

这些内容不一定都要学到算法研究层面,但作为架构师,至少要知道它们能做什么、不能做什么、适合放在系统里的哪个位置、会带来哪些工程风险。

这次软考高级架构师考试给我的一个提醒是:

架构师的知识边界正在被 AI 拉宽。

选择题里出现 Transformer 和多模态,论文题里出现向量数据库和多模态大模型,这些都说明 AI 已经逐渐进入软件架构的主干知识里。

对我来说,选择题感觉还可以,案例题仍然需要继续练。更重要的是,后面复习和学习时,不能只看传统架构内容,也要把 AI 工程化、大模型应用架构、向量检索和多模态场景补起来。

考试只是一个节点。

真正值得记录的,是它让我看到技术趋势已经走到试卷上了。

大规模无状态爬虫系统设计

这篇文章记录的是一套大规模无状态爬虫系统的设计。

先说明一下参与边界:这套系统不是我一个人独立设计完成的。我主要负责爬虫端的核心设计和实现,另一位同事是项目主要负责人,他有十余年的架构设计经验,整体系统设计、调度中心以及很多关键取舍都由他主导。我在这个系统里更多是站在爬虫端视角,参与了一套大规模无状态爬虫体系的落地。

也正因为那时自己还是初入职场,所以这套设计对我的意义不只是“写了一个爬虫”,而是第一次比较完整地看到:爬虫在工程系统里不应该只是脚本,它可以是一个被调度、被扩容、被监控、可替换的采集节点。

本文只讨论在授权和合规范围内的数据采集系统设计,不涉及绕过站点安全机制或采集敏感数据。

在做这个项目之前,Scrapy 是很自然会被想到的方案。

它有成熟的爬虫生命周期、调度器、下载器、中间件、管道、去重、状态管理等能力。对于中小规模、结构清晰、业务变化不频繁的采集任务来说,Scrapy 确实是一套完整方案。

但这套系统面对的问题不太一样。

我们更关注的是大规模任务下的采集吞吐、任务调度、账号分配、异常处理和快速扩容。Scrapy 自带的体系虽然完整,但学习成本较高,入手较慢,架构也相对复杂。尤其当系统需要把任务状态、账号状态、异常流转、代理分配、补偿处理这些能力统一放到一个调度中心管理时,爬虫本身再保留太多状态,反而会让边界变得不清楚。

所以最后的方向是:不沿用 Scrapy 的架构模式,而是结合现有高并发框架,设计一套更轻、更快、更容易水平扩展的无状态爬虫系统。

爬虫核心只负责一件事:拿到任务后尽快完成数据抓取,包括必要的增量更新,然后把结果交给后续链路。

按当时的架构草图抽象后,整体链路大概是这样:

大规模无状态爬虫系统架构图
任务、账号、代理由调度中心统一下发;爬虫节点保持无状态,采集结果进入 Kafka、Flink、ES 数据链路。

在这个体系里,Java 服务承担调度中心的角色。它负责任务协调、账号分配、账号状态管理、异常状态流转、代理下发等能力。

爬虫端则被刻意设计得很薄。

爬虫启动后向调度中心领取任务。调度中心在下发任务时,会同时给出这次采集所需的账号和代理。爬虫拿到这些一次性上下文后开始采集,采集完成后把数据写入 Kafka,并向调度中心汇报任务结果和心跳状态。

这里的“一次性”不是指账号用一次就丢弃,而是指一次采集任务内绑定一次任务、账号和代理。任务结束后,账号会根据结果重新回到有效账号池,或者进入异常账号池,等待专门的登录模块重新处理。

我觉得这套系统里最关键的设计,就是把爬虫做成无状态。

传统爬虫经常会在自己内部维护很多信息:当前任务跑到哪一步、账号是否可用、代理是否失效、失败后要不要重试、异常应该怎么处理、下次从哪里继续等。

这些能力当然有价值,但如果所有爬虫节点都各自维护状态,系统规模一大,就会出现几个问题:

  • 单个爬虫节点变重,扩容和迁移成本变高。
  • 账号、代理、任务状态分散在各处,难以统一判断。
  • 某个节点异常退出后,恢复逻辑复杂。
  • 错误处理混在采集逻辑里,爬虫代码越来越难维护。

无状态的思路是反过来的:爬虫只负责执行当前任务,不负责长期持有状态。

它不决定一个账号后续应该怎么处理,也不决定一个异常任务最终怎么补偿。它只把采集过程中的结果、错误和心跳上报给调度中心,由调度中心再调度给对应的处理模块。

这样做以后,爬虫端会变得非常轻。

如果某个爬虫节点挂了,系统只需要感知它心跳消失,再把未完成任务重新调度出去。爬虫本身不需要承担复杂恢复逻辑。对于我当时负责的爬虫端来说,这个设计最大的好处就是:代码目标非常明确,采集就是采集,错误就是上报。

这套系统里,爬虫单次采集任务大约 15 秒左右就可以完成。

它能快起来,原因不只是“并发写得高”,更重要的是系统边界清楚。

调度中心已经提前准备好了任务、账号和代理,爬虫不需要在执行过程中再做大量决策。拿到任务后,爬虫可以直接进入采集流程。它只处理当前任务所需的请求、解析、增量判断和结果投递。

采集结果进入 Kafka 后,后面的清洗、聚合、存储交给 Flink 和 ES 链路。爬虫不在本地做过多处理,也不会把数据链路和采集链路耦合在一起。

从工程上看,这其实是在减少爬虫节点的职责。

节点职责越少,单次任务越短,失败成本也越低。即使某个任务失败,也可以快速上报并进入调度中心的异常处理流程,而不是让爬虫自己在本地反复纠缠。

爬虫选择 Docker 部署,是因为这个系统天然需要横向扩容。

如果爬虫直接跑在固定机器上,扩容会比较麻烦。新机器环境要配置,依赖要安装,版本要对齐,启动方式也容易不一致。Docker 把运行环境打包后,爬虫就可以在任意一台机器上快速启动。

这带来了两个非常直接的好处。

第一,可以一键扩容。

当采集任务变多,或者需要在短时间内提高吞吐时,只需要增加爬虫容器数量。因为爬虫是无状态的,新启动的容器不需要同步复杂上下文,只要能连上调度中心,就可以开始领取任务。

第二,可以按数据采集情况动态调整数量。

任务高峰期增加爬虫节点,任务低谷期减少节点。爬虫节点本身不保存长期状态,所以扩容和缩容都比较自然。

这也是无状态设计和容器化部署非常契合的地方:一个节点随时可以来,也随时可以走,系统的长期状态不依赖它。

从图上看,可能会有一个疑问:代理池去哪了?

实际设计里,代理也由调度中心负责。

爬虫在领取任务时,调度中心会把任务、账号、代理一起下发。对于爬虫来说,它不需要自己去代理池里挑选代理,也不需要判断某个代理是否还应该继续使用。它只需要使用调度中心给出的代理完成当前任务,并把结果反馈回去。

这样设计的好处是统一。

任务、账号、代理在一次采集里是绑定关系。如果采集失败,调度中心可以结合错误类型判断问题出在哪里:可能是任务本身异常,可能是账号失效,也可能是代理不可用。爬虫端只提供事实,不做最终裁判。

这让异常处理有了更清晰的入口。

账号管理是这个系统里非常重要的一部分。

有效账号池保存当前可用账号。调度中心给爬虫下发任务时,会从有效账号池里分配账号。任务完成后,如果账号表现正常,就重新回到有效账号池,等待后续继续使用。

如果采集过程中发现账号异常,爬虫不会自己尝试修复账号,而是把异常上报给调度中心。调度中心再把账号放入异常账号池,由账号登录模块或专门处理模块去恢复。

恢复成功后,账号重新进入有效账号池;恢复失败,则继续留在异常状态,等待后续处理或下线。

这套流转看起来绕了一步,但它让职责非常清楚:

  • 爬虫负责发现和上报异常。
  • 调度中心负责状态流转和资源分配。
  • 账号登录模块负责账号恢复。
  • 有效账号池只保留可用于任务分配的账号。

当系统规模变大时,这种职责拆分会比“爬虫自己判断一切”更稳。

爬虫采集到的数据不会直接写入最终存储,而是先进入 Kafka。

Kafka 在这里承担缓冲和解耦作用。爬虫只需要稳定地把采集结果投递出去,不需要关心后续清洗、转换和索引写入的具体细节。

Flink 负责消费 Kafka 中的数据,做实时清洗、转换、去重或补充处理。处理后的数据再写入 ES,供后续检索和查询使用。

这条链路的好处是采集和处理分离。

爬虫节点只追求采集效率,数据处理链路则可以按自己的节奏扩展。如果后续清洗逻辑变复杂,也不会直接拖慢爬虫侧的执行。

这套设计对我最大的影响,是让我第一次真正理解“少做一点”有时候是更好的工程设计。

刚开始做爬虫时,很容易觉得爬虫应该什么都管:任务、状态、账号、代理、重试、异常、存储,最好都封装在一个完整框架里。但在大规模系统里,爬虫越重,越容易变成难以扩展的节点。

这套系统反而让我看到另一种思路:

爬虫不需要成为系统中心。它可以只是一个高性能、可替换、可扩容的执行单元。真正的状态和调度逻辑,应该放到更适合统一管理的位置。

对当时初入职场的我来说,这个认知很重要。

我开始意识到,架构设计不是把所有能力都堆进一个模块里,而是决定每个模块应该知道什么、不应该知道什么。一个模块越清楚自己不负责什么,边界往往越稳定。

回头看,这套无状态爬虫体系最让我印象深刻的地方,就是它把复杂性从爬虫端拿走了。

爬虫只领取任务、执行采集、上报结果;调度中心统一管理任务、账号、代理和异常;数据进入 Kafka、Flink、ES 组成的后续链路。每一层都有自己的职责,每一层也都可以独立扩展。

这比单纯写一个“能跑的爬虫”要更接近真正的工程系统。