阅读视图
汇金资损防控体系建设及实践 | 得物技术
给Javaer看的大模型开发指南|得物技术
Cursor Rules优化实战:构建高效稳定的AI代码生成规范体系|得物技术
Redis 是单线程模型?|得物技术
得物社区活动:组件化的演进与实践
得物研发自测 & 前端自动化测试体系建设
从CPU冒烟到丝滑体验:算法SRE性能优化实战全揭秘|得物技术
得物自研DScript2.0脚本能力从0到1演进
社区造数服务接入MCP|得物技术
一、背景
今年 MCP 的概念非常火,市面上也涌现出了一大批 MCP 相关工具。作为技术一线者,都会按捺不住地去实操一下,很早的时候就有个设想,如果把我们的测试工具都改造为符合 MCP 服务协议标准,然后全部接入 AI Agent,打造一个集万千工具于一体的智能管家来帮助我们提效,是不是一个很完美的设想。很多宏伟或者天马行空的想法想要真正的落地,必然需要不断向下,拆解成可落地的任务模块,这里我们先从造数开始。
二、AI 造数设想
在实际业务需求测试中,我们依赖的测试数据需要很多前置的数据要求,这时候会涉及到分步使用不同的造数脚本。比如团长拉新做任务,需要一个 30 天内没发过动态的账号,加入团队,发一篇动态,动态过一审,过二审,阅读数满足 300 个。
为了完成这个场景的造数,我们需要去造数工厂、接口自动化、脚本代码等平台找对应的造数工具,分别去执行才能完成这一系列的操作。可以从下图中看到,总计需要 6 个步骤才能完成。如果不是熟悉所有的业务,哪怕有现成的造数脚本,组合起来使用还是有一定的门槛。
那么在 AI 风行的年代,我们想要实现的是:按照用户输入的测试数据要求,能够按照已有造数能力自动编排,生成对应的测试数据给用户使用。
最终实现效果案例:我需要一个团长拉新的测试数据,要求是 30 天内没有发过动态,进入团队 A,然后发布一条动态,需要过一审风控审核,二审标注,最后需要获得 300 个阅读数。
AI 造数自动去造数池子中寻找对应的造数接口,按照提问的顺序要求来依次执行造数,最后返回给用户对应的测试账号。
这里不再重复介绍 MCP 的概念,我们参考官方给出的 client-server 通用架构图来画一个 AI 造数的架构图,便于理解在落地到 AI 造数的场景,我们可以做哪些事。本篇文章主要就讲解了图中的其中一环,落地社区造数服务的 MCP 接入。
三、社区造数服务 tools
框架介绍
社区的造数服务技术栈是基于 FastAPI 框架实现的,通过 uv工具来管理依赖库、虚拟环境等,这个工具亲测的确比传统的 pip 或者 poetry 等工具更好用。从安装 uv到启动项目,只要 4 步就能无痛搞定环境,不用担心本地其他环境的干扰。
## uv命令1. 安装uv : `curl -LsSf https://astral.sh/uv/install.sh | sh`2. 创建环境 - 自定义环境名称和Python版本 `uv venv tools_venv --python 3.12`3. 激活环境 `source tools_venv/bin/activate`4. 安装依赖包 `uv pip install -r pyproject.toml`
## 本地启动项目直接运行main.py文件中的main方法即可,debug模式自己pycharm中设置if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)
中间件相关配置全部通过 ARK 来管理,项目结构如下:
## 项目结构
```bash
├── main.py # 启动 APP 入口文件
├── README.md # 开发手册
├── Dockerfile # Docker 镜像文件
├── alembic # alembic 迁移 DB 自动生成的相关文件
│ ├── README
│ ├── .env.py
│ ├── script.py.mako
│ └── versions # 存放每次迁移的版本,可用于回滚 DB 版本
├── alembic.ini # alembic 配置文件
├── app
│ ├── __init__.py # 注册 app
│ ├── api # api 开发目录
│ ├── core # app 的全局配置
│ ├── crud # 每个 table 的增删改查操作
│ ├── db # db 配置
│ ├── models # 存放表结构
│ ├── schemas # pydantic 模型
│ └── utils # 工具类
├── .pre-commit-config.yaml # 配置 git commit 时自动检测工具└── pyproject.toml # 依赖库管理```
统一部署到公司的发布平台,通过 http://{造数服务域名}/tools/docs#/ ,地址可以访问目前社区所有的造数接口。同时也对接了造数工厂,可以直接去造数工厂使用。
改造思路
老方案-基于 MCP Python SDK
早在出现 MCP 这个概念的时候,我就想过有天把我们的造数服务通过 MCP 工具暴露出来,这样就可以非常方便的集成各种 Agent,打造 AI 造数。在出现这个 FastAPI-MCP 框架之前,想要把造数服务改造成支持 MCP ,就需要通过引入 MCP 依赖库来实现。但这个方案对于已有的造数服务来说改造成本有些高,可以看老方案的案例。
从官方文档面向服务器开发者 - MCP 中文文档中可以找到有对应的 MCP Python SDK,主要就是安装 MCP 这个依赖库。这里举一个简单的 demo,通过手机号查询用户信息的方法。可以很清晰的看出来这个 SDK 的语法结构是需要 @mcp.tool() 这个装饰器来修饰,那么原有的造数服务暴露出来的所有接口方法是否都需要改造,这仍有一定的成本(未考虑其他复杂场景情况下)。
# server.pyfrom mcp.server.fastmcp import FastMCPfrom tools.tools_set import get_user_infoimport uvicorn# Create an MCP servermcp = FastMCP("Demo")
@mcp.tool()async def get_user_info_tool(mobile: str) -> Coroutine[Any, Any, Any]: """根据输入的手机号获取用户信息 Args: mobile: 手机号 """ info = get_user_info(mobile) return info
if __name__ == "__main__": """Initialize and run the server""" # mcp.run(transport="sse") """Start the FastAPI server with uvicorn""" uvicorn.run(app, host="0.0.0.0", port=8003)
基于上述代码 demo,我们通过 uvicorn 启动服务,当然也可以单独启动 MCP 服务。控制台输出如下,代表启动成功,接下来我们就可以使用 MCP 客户端工具进行连接使用了,这里使用 Cursor来做演示。
看图标显示绿色,无报错说明连接成功,这里也能看到 demo 中的 get_user_info_tool 方法作为 MCP 工具暴露了出来。演示到这里,说明了该方案是可行的。因为本文重点讲解采用的新方案,此处就不再多介绍,感兴趣的可以去看官方文档。
四、FastAPI-MCP
安装运行
“Expose your FastAPI endpoints as Model Context Protocol (MCP) tools, with Auth! ”
这是引用官网介绍的第一句话,翻译过来大概的意思就是:把你的 FastAPI 服务作为 MCP 工具暴露出来成为现实!
-
安装 FastAPI-MCP 库
uv add fastapi-mcp
oruv pip install fastapi-mcp
-
使用 FastAPI-MCP,只需要 3 行代码就能把 FastAPI 框架改造成一个 MCP 服务
-
通过 uvicorn 启动服务器,使用http://localhost:8000/mcp 来访问 MCP server
from fastapi import FastAPIimport uvicornfrom fastapi_mcp import FastApiMCP
# Create (or import) a FastAPI appapp = FastAPI()
# Create an MCP server based on this appmcp = FastApiMCP(app)
# Mount the MCP server directly to your appmcp.mount()
if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000)
用法介绍
自定义配置
通过看源码 FastApi-MCP 类,基本能清晰的看出来各个参数的用处,这里将介绍几个常用的。
class FastApiMCP: """ Create an MCP server from a FastAPI app. """ def __init__( self, fastapi: Annotated[ FastAPI, Doc("The FastAPI application to create an MCP server from"), ], name: Annotated[ Optional[str], Doc("Name for the MCP server (defaults to app.title)"), ] = None, description: Annotated[ Optional[str], Doc("Description for the MCP server (defaults to app.description)"), ] = None, describe_all_responses: Annotated[ bool, Doc("Whether to include all possible response schemas in tool descriptions"), ] = False, describe_full_response_schema: Annotated[ bool, Doc("Whether to include full json schema for responses in tool descriptions"), ] = False, http_client: Annotated[ Optional[httpx.AsyncClient], Doc( """ Optional custom HTTP client to use for API calls to the FastAPI app. Has to be an instance of `httpx.AsyncClient`. """ ), ] = None, include_operations: Annotated[ Optional[List[str]], Doc("List of operation IDs to include as MCP tools. Cannot be used with exclude_operations."), ] = None, exclude_operations: Annotated[ Optional[List[str]], Doc("List of operation IDs to exclude from MCP tools. Cannot be used with include_operations."), ] = None, include_tags: Annotated[ Optional[List[str]], Doc("List of tags to include as MCP tools. Cannot be used with exclude_tags."), ] = None, exclude_tags: Annotated[ Optional[List[str]], Doc("List of tags to exclude from MCP tools. Cannot be used with include_tags."), ] = None, auth_config: Annotated[ Optional[AuthConfig], Doc("Configuration for MCP authentication"), ] = None, ): ...
※ Server metadata
- name:MCP 服务名
- description:对 MCP 服务的描述
※ Tool and schema descriptions
创建 MCP 服务器时,可以通过修改 describe_all_responses ,把所有可能的响应模式包含在工具描述中,或通过更改 describe_full_response_schema 把完整的 json 包含在工具描述中。
from fastapi import FastAPIfrom fastapi_mcp import FastApiMCP
app = FastAPI()
mcp = FastApiMCP( app, name="My API MCP", description="Very cool MCP server", describe_all_responses=True, describe_full_response_schema=True)
mcp.mount()
※ Customizing Exposed Endpoints
- include_operations , 暴露 operation_id=XXX 的接口
- exclude_operations , 排除 operation_id=XXX 的接口
- include_tags , 暴露 tags=XXX 的接口
- exclude_tags ,排除 tags=XXX 的接口
组合使用:
- include_operations 和 exclude_operations 不能同时使用
- include_tags 和 exclude_tags 不能同时使用
- include_operations 和 include_tags 可以组合使用,匹配任一个条件就满足
from fastapi import FastAPIfrom fastapi_mcp import FastApiMCP
app = FastAPI()
# 案例1:include_operationsmcp = FastApiMCP( app, include_operations=["get_user", "create_user"])
# 案例2:exclude_operationsmcp = FastApiMCP( app, exclude_operations=["delete_user"])
# 案例3:include_tagsmcp = FastApiMCP( app, include_tags=["users", "public"])
#案例4:exclude_tagsmcp = FastApiMCP( app, exclude_tags=["admin", "internal"])
# 案例5:Combinedmcp = FastApiMCP( app, include_operations=["user_login"], include_tags=["public"])
mcp.mount()
工具命名
FastAPI 中的路由通过 operation_id 参数来作 MCP 工具名称,如果没有显示命名,框架会自动生成一个。此处经测试,如果不显示命名,自动生成的名字不仅会很奇怪,还会影响 AI 造数的准确性,所以这里最好作好规范,必须要显示命名。
# Auto-generated operation_id (something like "read_user_users__user_id__get")@app.get("/users/{user_id}")async def read_user(user_id: int): return {"user_id": user_id}
# Explicit operation_id (tool will be named "get_user_info")@app.get("/users/{user_id}", operation_id="get_user_info")async def read_user(user_id: int): return {"user_id": user_id}
五、接入造数服务
框架升级及改造
在接入的时候,要查一下官方文档要求的 Python,FastAPI 等版本,先进行框架升级,防止出现不兼容的问题。这项通过管理工具安装依赖库时能自动校验,其他一些兼容问题在启动服务后根据实际场景一一去解决即可。这里推荐使用 uv 工具进行管理,亲测比之前的 poetry 更好用。
列几个核心库的版本,都是验证过没有兼容问题的。在过程中也是遇到一些兼容问题花了点时间,因为 FastAPI-MCP 框架比较新,网上资料还不全,遇到没法解决的问题大家可以去项目 issue 中找,提升解决问题效率。
python = "^3.12"fastapi = "0.115.12"fastapi-mcp ="0.3.1"mcp="1.7.0"pydantic = "^2.11.0"pydantic-settings = "^2.2.0"
步骤
第一步: 引入 fastapi-mcp
第二步: main.py 中添加 MCP 服务
第三步: 也是工作量最大的一步,将每个造数接口都做显示命名,并且做好文档注释,写的越清楚 AI 造数的准确率越高,需要对应编写造数场景测试,共同完成
最后一步: 启动服务 uvicorn.run('main:app', host='0.0.0.0', port=8023, reload=True, workers=2) ,无报错基本就没有问题了。再通过 MCP 客户端工具连接使用即可
接入 Cursor
改造完之后的造数服务成功对外暴露了 MCP 服务,现在我们可以通过 MCP 客户端去连接使用了,这里选用了 Cursor,因为 Cursor 使用的人比较多,同时集成了市面上的主流大模型。
步骤
第一步: 创建一个 mcp.json,按照标准 json 配置即可
{ "mcpServers": { "fastapi-mcp": { "url": "http://localhost:8022/mcp", "description": "本地开发环境MCP服务配置" }, "tools-mcp": { "url": "http://localhost:8011/mcp", "description": "本地开发环境MCP服务配置" }, "demo-mcp": { "url": "http://localhost:8001/sse", "description": "本地开发环境MCP服务配置" }, "tools-mcp-prod": { "url": "http://XXXXXX/mcp", "description": "线上" }}}
第二步:点击右上角设置 icon,进入 Cursor Settings,选择 MCP
第三步: 这里可以看到,在刚才 mcp.json 中配置的 MCP工具均加载过来,打开开关,运行状态显示为绿色,无报错并说明了服务接入正常,接下来就可以正常使用 Cursor 中的 Agent 进行对话了
实操演练
我们现在只希望使用造数能力,因此我们可以指定刚才配置的 MCP 工具。
场景化案例
需求:给手机号为 11120210001 的用户发布一个点评动态,并且通过风控一审。
这里注意一下提问方式,因为我们没有对大模型进行特别的训练,AI 不一定知道 111 开头的是我们测试使用的虚拟手机号,有可能会误解为 userId,所以我们需要告诉 AI 这是一个手机号。
可以看到在这个 demo 中, Agent 自动帮我们分了三步去调用对应的 MCP tool,第一步通过我们输入的手机号去获取 userId,第二步通过 userId 去发布点评动态,第三步通过点评动态 id 去通过风控一审。原本需要三步完成的造数场景,现在通过一句话描述就完成了。
调优案例
需求:随机创建 10 个测试账号
※ 调优之前
造数代码,主要看文档注释内容。
@router.post('/create-account', operation_id="create_account",summary="创建测试账号")async def c_create_account( env: str = Body(..., description='环境'), phonenumber: str = Body(..., description='手机号'), pwd: str = Body(..., description='密码'), usernum: str = Body(None, description='数量'),) -> Any: """ 创建测试账号,默认111开头 args env: 环境,默认:t1 phonenumber: 手机号 pwd: 密码,默认:test123 usernum: 数量 """
把这个造数需求发送给 AI,发现报错了。我们去代码中看下为何返回了 false,原来是因为接口返回非 200,排查下来是因为 t1 环境测试账号造数默认填了 111,不需要再加 111,所以接口直接 500 了。
这里 AI 犯了两个错误:
- 因为默认手机号都是 11 位的,这里 AI 不知道只需要传 8 位就行。
- 我没有输入具体的手机号,所以按照代码逻辑应该是支持自动随机生成的,但是 AI 也不知道这个逻辑,“自作主张”给我传入了一个手机号。
※ 调优后
通过排查我们已经明确知道 AI 犯了哪些错误,那么我们针对这些错误去调优即可。所谓的调优主要就是修改文档注释,可以前后对比下注释内容。
"""创建测试账号,默认111开头,不用填写111,只需要后面8位不传手机号phonenumber,默认随机生成手机号
args env: 环境,默认:t1 phonenumber: 手机号,非必填,不填自动生成 pwd: 密码,默认:test123 usernum: 数量"""
※ 最终效果
通过这个案例可以看到,准确率依赖我们对造数接口的文档注释,所以在实际使用过程中,前期需要我们不断地去调优,才能达到我们想要的效果。
当然随着后续迭代,可能可以用更优雅的方式完成这个工作,比如再引入静态代码分析工具,通过 AI 编程自动完成注释。
六、总结
技术实践成果
通过将社区造数服务改造成符合 MCP(Model Context Protocol) 标准的工具,我们成功实现了以下目标:
AI 驱动的测试数据自动化
用户通过自然语言描述需求,AI Agent 可自动编排造数接口生成复杂测试数据,将原本需手动执行 3 步的操作简化为一步指令。
低成本框架升级
基于 fastapi-mcp 框架,仅需少量代码改造即可将 FastAPI 服务快速接入 MCP 协议,解决了传统 SDK 方案的高适配成本问题。
工具链整合
通过对接 Cursor 等 AI 工具平台,验证了 MCP 协议在跨平台协作中的可行性,为后续构建“社区智能管家”奠定技术基础。
核心实践经验
注释即规范
AI 调用接口的准确性高度依赖代码注释的清晰度。通过优化接口文档(如参数默认值、输入格式说明),可显著提升 Agent 的任务解析成功率。
渐进式调优
初期需通过人工干预优化 Agent 的接口调用逻辑,未来可引入代码静态分析工具自动生成标准化注释。
未来优化方向
动态编排增强
当前接口调用为线性执行,后续可探索基于依赖关系的动态编排(如并行执行独立步骤、自动重试失败操作)。
多 Agent 协作
结合领域知识库与测试断言工具,实现从“造数”到“验证”的全链路 AI 自治。
协议扩展性
探索 MCP 与更多协议(如 OpenAPI)的互操作性,提升工具服务的跨平台复用能力。
价值与启示
本次实践印证了 “AI+协议化工具” 在测试领域的巨大潜力:降低技术门槛 (非技术人员可直接描述需求)、提升执行效率 (分钟级操作秒级完成)、释放创新空间 (复杂场景的自动化长链路测试)。
随着 MCP 生态的完善,测试工程将逐步从“工具堆砌”走向“智能协作”,为研发效能带来质的突破。
七、感想
“我不是英雄,只是一个拿锤子的约德尔人”
站在巨人的肩膀上总是能看的更高更远,追随技术大牛们的步伐,把 AI 应用到工作中、生活中。回想九年前初入测试行业时捧读的《Google 软件测试之道》,书中“人类智慧的最后一英尺”已然越来越近。重读了 2022 年在公司内部博客发表的《Google 软件测试之道:结合实践的总结》一文,发现仅仅过了3 年,如果现在再去写,又是完全不一样的想法了,技术的发展已发生翻天覆地的变化。
此刻回望测试领域的演进曲线,愈发感到:「拿锤者」的价值不在于挥舞工具的姿态,而在于持续校准认知坐标的能力 。当 AI 重构测试链路的每个环节时,唯以「锤者」的务实与「巨人」的视野双轨并行,方能在技术洪流中锚定价值支点。
加油吧!不忘初心,你我终将能抵达一个又一个“终点”!
往期回顾
3.以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
4.得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 阿凯
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
CSS闯关指南:从手写地狱到“类”积木之旅|得物技术
一、背景
在Web开发网页设计中,CSS(层叠样式表)扮演着至关重要的角色,它用于控制网页的布局、外观和视觉效果。CSS不仅可以美化网页的视觉表现,还可以提高网页的可访问性、可维护性和响应式设计。在我们进行网页开发的时候,CSS是必不可少的一个环节。但是在早期的纯手写CSS阶段时会存在很多的痛点,这些痛点催生了 CSS 预处理工具(如 Sass/Less)和 CSS-in-JS 方案的兴起,进入工具曙光时代,但它们本质上仍未能突破"手动编写样式规则"的范式。直到原子化 CSS 理念的回归——通过预定义的实用类(Utility Classes)组合样式,配合智能化的工具链,为解决传统 CSS 困境提供了新的思路。
二、纯手写CSS的黑暗年代
在前端开发的早期阶段,一直以"纯手写"的方式主导着开发者的工作流。我们习惯于在 .css 文件中逐行编写选择器,包含布局控制,视觉设计,响应式设计,动画交互效果,可访问性等等一些列的关键要素,通过类名、ID 或标签选择器来定义样式规则。这种方式看似直观,但随着项目规模的扩大和团队协作的深入,传统手写 CSS 的局限性逐渐暴露如下的一些问题。
代码冗余与维护成本
每个元素的样式都需要手动编写,开发者会陷入一个“复制粘贴炼狱”的困境,同时也会导致大量重复代码。例如一个简单的 flex 布局需要在多个组件中重复定义 display: flex; justify-content: center; align-items: center; ,项目体积无意义膨胀。最经典的当属于按钮,输入框等表单样式的定义,这些元素在我们进行网页开发的时候是非常常见的,尤其是后台管理页面的开发。典型表现为:
/* 重复定义的按钮样式 */.primary-btn { padding: 8px 16px; background: #42B983; border-radius: 4px; color: white;}
.submit-button { /* 相同样式不同命名 */ padding: 8px 16px; background: #42B983; border-radius: 4px; color: white;}
/* 散落在各文件的边距定义 */.article-list { margin-bottom: 24px;}
.mb24 { margin-bottom: 24px; /* 相同值重复定义 */}
/* 后续迭代新增 */.section-spacing { margin-bottom: 24px; /* 开发者可能已忘记已有定义 */}
这种代码冗余导致三重灾难
※ 文件体积失控
导致页面的CSS文件大小达到MB的级别,而且其中很多都是重复规则。
※ 修改成本倍增
调整基础间距值时,开发者需要在多个位置进行修改。
※ 增加认知负担
开发者需要记忆 margin-bottom: 24px 可能存在于 mb24 、 section-spacing 等多种不同实现。
上下文割裂的开发体验
在传统开发前端页面时,一般在 .html 文件中定义页面结构, .css 文件中定义页面的样式,所以开发时需要频繁在 HTML 模板文件和 CSS 文件之间切换,特别是在现代组件化框架中,这种割裂感更加明显。查看某个元素的样式需要跨文件检索,打断编码心流,影响开发的效率。例如下面一个React组件:
{/* 社交链接组件 */}<ul class="social-links" style={{ marginBottom: 24 }}> <li><a href="https://twitter.com/yourcompany" target="_blank">Twitter</a></li> <li><a href="https://facebook.com/yourcompany" target="_blank">Facebook</a></li> <li><a href="https://linkedin.com/company/yourcompany" target="_blank">LinkedIn</a></li></ul>
// 组件.less.social-links { margin-bottom: 10px; // 组件内部的CSS > li + li { margin-top: 5px; }}
// 全局的.lessul { margin: 0; // 全局重置}
这个 ul 元素的样式定义在了三个地方,有时候修改样式的时候,我们需要进行切换到不同文件才能修改元素的样式,定位成本也会剧增。
命名困境与样式污染
类名设计逐渐演变成哲学问题——既要语义化(.user-card-container)又要避免冲突,最终演变成冗长的 BEM 命名( .user-card__avatar--rounded )。即便如此,全局作用域仍可能导致样式意外覆盖。
/* 经典BEM实践 */.user-card__avatar-container--rounded { /* 长度达39字符 */ border-radius: 50%; overflow: hidden;}
/* 应对主题化的变异 */.user-card__avatar-container--rounded-dark-mode { /* 突破50字符 */ filter: brightness(0.8);}
/* 组件库维护者的绝望 */.namespace-user-card__avatar-container--rounded-primary-theme-2024 { /* 类名已成为密码学谜题 */}
BEM 命名方式
在一定程度上能缓解命名冲突,但是也会带来一些新的问题:
※ 命名长度失控
企业级项目中平均类名长度可达30+字符。
※ 可读性悖论
过度细化的命名反而导致理解成本上升,开发者需要很长时间才能解析这单个类名。
※ 重构噩梦
当我们需要重命名 user-card 组件时,那我们可能需修改N个相关类名。
响应式与动态样式的笨重实现
面对复杂的响应式需求时,传统 CSS 需要编写多个媒体查询区块;动态样式(如颜色主题切换)往往依赖 CSS 变量或预处理器的混入,增加了架构复杂度。例如:
/* 典型响应式布局实现 */.card-container { width: 100%; margin: 10px;}
@media (min-width: 640px) { .card-container { width: 50%; margin: 15px; }}
@media (min-width: 1024px) { .card-container { width: 33.33%; margin: 20px; }}
/* 针对深色模式的叠加修改 */@media (prefers-color-scheme: dark) { .card-container { background: #1a1a1a; }}
此类代码导致
※ 维护黑洞
单个组件每增加一种响应式的设备,响应式代码可能需要在原有的样式代码上翻一倍。
※ 调试困境
调试困境:开发者需同时关注视口尺寸、设备类型、主题状态等多个变量。
样式与结构分离的代价
虽然关注点分离是良好实践,但在现代组件化开发中,过度分散的样式定义反而降低了组件的内聚性。当需要修改组件样式时,开发者不得不同时维护模板文件和样式文件。
三、工程化曙光
原生CSS开发曾因全局作用域污染、样式冗余和维护成本高昂等问题让开发者备受煎熬,技术演进催生出多种解决方案体系:Sass/Less等预处理器通过变量机制和嵌套语法实现样式逻辑抽象,使代码复用率提升60%;CSS Modules借助哈希类名构建本地作用域,从根本上消除样式冲突隐患;BEM命名规范采用模块化语义结构,将团队协作效率提升70%;随着组件化开发范式普及,CSS-in-JS方案通过样式与组件的深度绑定,实现动态主题切换等复杂需求,使React组件复用率突破90%。这一系列工程化实践推动CSS从手工模式迈向标准化协作体系,构建起现代前端开发的样式基础设施,给我们至暗的纯手写时代带来了一束光明。
CSS预处理器的救赎
CSS预处理器(CSS Preprocessor)是一种通过扩展语法+编译工具,让开发者能用编程思维写样式的方案。其核心功能具有变量、嵌套、函数、混合(Mixin)、继承、运算等编程特性,使得CSS更加灵活和强大。为传统CSS注入了工业化基因。采用Sass的项目代码复用率提升至50%+,CSS体积平均缩减40%+,标志着样式开发进入"工程化觉醒"时代。
救赎改进点
CSS预处理器给开发者带来了如下的曙光:
※ 代码复用革命
// 变量系统 - 设计Token统一管理 $primary-color: #42B983; $spacing-unit: 6px;
// Mixin工厂 - 封装复用逻辑 @mixin flex-center { display: flex; justify-content: center; align-items: center; }
// 继承体系 - 避免重复定义 %button-base { padding: $spacing-unit * 2; border-radius: 4px; }
.submit-btn { @extend %button-base; background: $primary-color; @include flex-center; }
能够抽取公共样式,定义变量,能够做到一处修改处处生效,提高代码复用率。
※ 结构嵌套优化
.navbar { padding: 12px; &__item { margin-right: 20px; &--active { color: $primary-color; } } } /* 编译后 */ .navbar { ... } .navbar__item { ... } .navbar__item--active { ... }
嵌套语法将代码组织效率提升,但需警惕过度嵌套导致选择器层级膨胀等问题。
※ 逻辑控制能力
// 条件语句动态生成主题 @mixin theme($mode) { @if $mode == dark { background: #1a1a1a; } @else { background: #ffffff; } }
// 循环生成间距工具类 @for $i from 1 through 8 { .mt-#{$i} { margin-top: $spacing-unit * $i; } }
增加编程的思路,能够定义变量,支持条件判断语句和循环的能力,一定程度上减少代码的体积,增加可阅读性。
曙光中的阴影
尽管预处理器大幅提升了样式工程能力,但仍存在本质性局限:
※ 工具链依赖困境
必须依赖Node.js/Ruby等编译环境,需要借助一些编译工具将CSS预处理器的语法编译成浏览器能够识别的CSS语法,同时编译时长随项目规模线性增长,编译后的代码量和纯手写的代码量区别不是很大,一定程度上也会影响页面的加载。
※ 浏览器兼容性断层
# 开发环境需实时编译 sass --watch input.scss:output.css
浏览器无法直接解析 .scss 文件,导致热更新延迟,以及无法在浏览器控制台直接编辑源码等相关的问题。
※ 作用域污染无解
// 编译后的CSS仍是全局样式 .navbar__item--active { ... } // 其他组件可能定义相同类名导致冲突
Sass仅提供语法糖,未改变CSS底层作用域模型,全局样式污染的问题存在。
※ 上下文割裂加剧
<!-- HTML模板 --> <div class="navbar"> <div class="navbar__item navbar__item--active"></div> </div>
<!-- 对应的SCSS文件 --> /* styles/navbar.scss */ .navbar { ... }
开发者仍需在结构层与样式层之间反复切换,认知断层率无法有效得到解决。
CSS命名规范实践
CSS(层叠样式表)命名规范是确保CSS代码结构清晰、易于维护和可扩展的关键。遵循一套明确的命名约定可以大大提高团队协作的效率,减少样式冲突,并使代码更加可读。常见的CSS命名规范有:BEM规范、SMACSS规范、OOCSS规范。
BEM规范
将CSS类名分为块、元素和修饰符三个部分。举个例子:
<div class="block"> <h2 class="block__title">标题</h2> <ul class="block__list"> <li class="block__list-item">列表项1</li> <li class="block__list-item block__list-item--highlighted">列表项2</li> </ul></div>
其中block代表一个组件或UI部件, block__title 和 block__list 代表块的子元素, block__list-item 代表列表项。 block__list-item--highlighted 是一个修饰符,表示该列表项被突出高亮显示。
SMACSS规范
SMACSS不仅仅是命名规范,还包括CSS文件结构的组织规范。SMACSS主要是将样式分成五大类,分别是Base、Layout、Module、State、Theme。其中:
- Base类主要是基本样式规则,例如重置浏览器默认样式、设置全局的基本样式等。这些样式通常以选择器(标签选择器、通用选择器)为基础,并且适用于整个项目。
- Layout类用于创建页面布局和网格系统,它定义了页面的整体结构、栏目布局、容器和网格样式等。
- Module类用于定义可重复使用的模块样式。
- State类用于定义组件的状态样式,如 .btn 和 .btn-primary 的样式。
- Theme类主要是主题相关的样式,如 .site-title 和 .module-title 的样式。
/* Base */a { color: #42B983; text-decoration: none;}
/* Layout */.container { width: 90%; margin: 0 auto; padding: 0 15px;}
/* Modules */.btn { display: inline-block; padding: 10px 20px; margin: 5px 0; border: none; border-radius: 5px; cursor: pointer;}
.btn-primary { background-color: #42B983; color: #fff;}
/* State */.btn:hover { background-color: #0056B3;}
.btn:disabled { opacity: 0.6; cursor: not-allowed;}
/* Theme (Optional) */.theme-dark { background-color: #333; color: #fff;}
OOCSS规范
OOCSS规范主要遵循结构(Structure)与外观(Skin)分离的原则,例如:
<div class="box box-red">你好</div><div class="box box-blue">OOCSS规范</div>
其中结构部分用 .box ,外观部分用 .box-red 来命名。
CSS命名规范优点
※ 避免冲突
合理的命名可以减少CSS选择器之间的冲突,特别是在大型项目中,这可以避免不必要的样式覆盖问题。
※ 可维护性
良好的命名规范使得代码更容易理解和维护。当团队中的成员或者未来的你(在几个月或几年后)需要修改或扩展样式表时,清晰的命名会大大减少认知困惑和错误。
※ 可读性
清晰、一致的命名风格可以提高代码的可读性。这对于快速定位问题或添加新功能至关重要。
※ 可扩展性
通过使用有意义的命名,你可以更容易地预见未来的需求变化,并设计出能够轻松扩展的样式表。
※ 团队协作
在团队项目中,统一的命名规范可以减少沟通成本,使得不同成员之间的工作更加协调和高效。
※ 语义化
好的命名应该反映元素的功能或内容,而不是仅仅基于其外观。这有助于开发者更好地理解每个样式的用途和作用。
CSS模块化方案
在CSS模块化中,CSS模块化是一个CSS文件在JavaScript中的一种使用方式,它允许你使用本地作用域的CSS类名,从而避免了全局命名空间污染的问题。CSS模块化通过Webpack等模块打包工具实现,使得CSS文件能够以模块的形式导入到JavaScript文件中,进而在React、Vue等现代前端框架中使用。一些常见CSS模块化的方案包括Vue里的scoped方案,CSS Modules with React方案。
CSS Modules with React
需要借助Webpack等编译工具,再结合 css-loader ,在Webpack配置文件中添加相应的loader:
module.exports = { module: { rules: [ { test: /.css$/, use: [ 'style-loader', // 将JS字符串生成为style节点 { loader: 'css-loader', // 将CSS转化成CommonJS模块 options: { modules: true // 开启CSS Modules } } ] } ] }};
在你的React组件或其他JavaScript模块中,你可以这样导入和使用CSS Modules:
import styles from './index.module.css';
export default function Container() { return <div className={styles.container}>Hello World</div>;}
在 index.module.css 中,你可以定义CSS类:
.container { display: block;}
CSS模块化优点
※ 作用域化
每个类名在编译时会被转换成唯一的标识符,避免了全局命名冲突。
※ 组合
可以使用 :global 或 :local 伪类来指定全局或局部样式。
例如: :global(.someClass) 会将 .someClass 定义为全局样式,类名不会转换成唯一的标识符。
※ 变量
可以使用CSS变量(自定义属性),例如 --main-color ,在模块内部使用。
※ 嵌套
虽然CSS Modules本身不支持CSS的嵌套语法(如Sass的嵌套),但你可以通过预处理器如Sass或Less来实现嵌套,然后通过相应的loader(如 sass-loader 或 less-loader )处理。
CSS-in-JS方案
CSS-in-JS是一种将CSS样式直接写入JavaScript代码中的方法,通过将样式与组件绑定,可以避免全局样式的冲突问题。一些常见的CSS-in-JS解决方案包括Styled Components、Emotion和JSS等。
Styled Components
styled-components 是一个流行的 CSS-in-JS 库,用于在 React 或其他 JavaScript 应用中编写组件级样式。它通过将 CSS 直接嵌入 JavaScript/TypeScript 代码中,实现了样式与组件的紧密绑定,同时支持动态样式和主题管理。其核心特性如下所示:
※ 组件化样式
样式与组件一一对应,避免全局 CSS 的命名冲突问题。
import styled from 'styled-components';
const Button = styled.button` background: ${props => props.primary ? 'blue' : 'gray'}; color: white; padding: 10px 20px; border-radius: 4px;`;
// 使用<Button primary>Click Me</Button>
※ 动态样式
支持通过 props 或全局主题动态调整样式。
const Text = styled.div` color: ${props => props.theme.primaryColor}; font-size: ${props => props.large ? '20px' : '16px'};`;
※ 自动 Vendor Prefixing
自动为 CSS 属性添加浏览器前缀(如 -webkit- , -moz- )。
※ 主题支持
通过 全局传递主题变量。
import { ThemeProvider } from 'styled-components';
const theme = {primaryColor: '#007bff'};
<ThemeProvider theme={theme}><App /></ThemeProvider>
CSS-in-JS方案优点
※ 作用域隔离
通过自动生成的唯一类名,可以避免全局CSS命名冲突。
※ 组件化
CSS直接与React组件(或其他JavaScript框架/库的组件)绑定,使得样式与组件逻辑紧密相关联。
※ 动态样式
可以更方便地根据组件的props动态生成样式。
※ 易于维护
与组件代码放在一起,便于管理和维护,减少文件之间来回切换的成本。
四、原子化革命
原子化CSS是一种将样式拆解为最小功能单元的CSS方法论,每个类名对应单一的CSS属性(如 .m-4 表示 margin:1rem , .text-red 表示 color:red ),通过组合多个原子类快速构建界面样式,既提升代码复用性又减少冗余。
其核心思想是通过预设的设计系统(如间距、颜色、字号等规则)生成原子类,确保视觉一致性并加速开发。常见的框架包括Tachyons,Tailwind CSS、UnoCSS和Windi CSS,它们通过工具自动生成原子类库,适用于中大型项目、设计系统及需要高维护性和性能优化的场景。下面是原子化CSS框架演进路线图表格:
Tailwind CSS
Tailwind CSS 是一种流行的原子化 CSS 框架,通过提供预设的实用类(Utility Classes),允许开发者直接在 HTML 中组合类名来构建界面,无需手动编写传统 CSS 代码。它的核心理念是“通过组合原子类实现设计,而非自定义样式”,强调高复用性、设计一致性和开发效率。
使用流程
※ 快速初始化与配置
# 创建基础工程 npx create-react-app my-app --template tailwind # 配置文件生成 npx tailwindcss init -p
在 tailwind.config.js 中定义设计系统约束:
module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: { colors: { primary: "#42B983", // 主题色 }, }, }, };
※ 原子类组合开发
// React 组件示例 function ProductCard({ title, price }) { return ( <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-all"> <h3 className="text-xl font-bold text-gray-800 mb-2">{title}</h3> <div className="flex items-center justify-between"> <span className="text-brand text-2xl">${price}</span> <button className="bg-brand text-white px-4 py-2 rounded-md hover:bg-blue-600"> 立即购买 </button> </div> </div> ); }
无需维护独立 CSS 文件,所有的CSS都通过原子类的形式添加到元素上,间距、颜色等设计决策直接映射到类名,可以在元素上直观的查看元素的布局,大小,颜色的特性;通过 md:grid-cols-3 等前缀声明断点逻辑,支持很好的响应式方式。
对比传统开发模式的核心优势
※ 消除样式冗余与全局污染
- 传统模式:
/* styles/button.css */ .btn-primary { padding: 12px 24px; background: #3b82f6; color: white; border-radius: 8px; }
/* styles/card.css */ .card-header { padding: 12px 24px; /* 重复定义 */ background: #3b82f6; /* 与按钮颜色耦合 */ }
- Tailwind 模式:
<button class="px-6 py-3 bg-blue-500 text-white rounded-lg"> <header class="px-6 py-3 bg-blue-500">
使用 Tailwind CSS 可以快速构建出现代化的网站和应用程序。通过使用预定义的原子类,开发人员可以快速地创建各种样式,而不必手动编写大量的 CSS 代码,提高代码复用率,减少冗余代码,减少项目体积,同时的话很好的解决命名冲突的问题。
※ 提升响应式开发效率
- 传统媒体查询:
.container { width: 100%; } @media (min-width: 768px) { .container { width: 50%; } }
- Tailwind 方案:
<div class="w-full md:w-1/2"></div>
※ 可自由高度定制性
Tailwind CSS 提供了丰富的配置选项,允许开发人员根据项目需求进行自定义。你可以修改颜色、字体、间距、阴影等各种样式属性,使得 Tailwind CSS 可以适应各种设计风格和品牌标识。
// tailwind.config.js module.exports = { content: [ "./pages/**/*.{js,ts,jsx,tsx}" ], darkMode: "class", theme: { extend: { colors: { "dark-blue": "#11151C", "light-gray": "#22262D", primary: "#43a4fe", "primary-100": "#f0faff", "primary-200": "#e6f6ff", "primary-300": "#bde6ff", // ... danger: "#FF3D71", divider: "#9AA5B0", light: "#C9D1D9", "input-bg": "#11151C", "addon-bg": "rgba(0, 0, 0, 0.02)", }, boxShadow: { "inset-left": "inset 10px 0 8px -8px #00000026", "inset-left-dark": "inset 10px 0 8px -8px #C9D1D920", "inset-right": "inset -10px 0 8px -8px #00000026", "inset-right-dark": "inset -10px 0 8px -8px #C9D1D920", }, screens: { "3xl": "1920px", }, keyframes: { heartBeat: { "0%, 50%, 100%": { transform: "scale(1)", }, "25%, 75%": { transform: "scale(1.3)", }, }, }, spacing: { 108: "27rem", 120: "30rem", 132: "33rem", }, }, }, plugins: [ require("@tailwindcss/forms"), require("@tailwindcss/line-clamp"), require("tailwind-scrollbar"), ], variants: { scrollbar: ["rounded"], },};
尽管 Tailwind CSS 提供了大量的预定义原子类,但它仍然非常灵活,允许开发人员根据需要进行定制和扩展。你可以根据项目需求添加自定义的原子类,或者通过配置文件修改默认的样式设置。
※ 强制执行设计规范
- 通过配置约束消除像素级自由定义:
// tailwind.config.js spacing: { 0: '0', 1: '4px', 2: '8px', // 禁止使用非标值 }
可以确保项目中的样式保持一致性。通过在整个项目中重复使用相同的原子类,可以确保不同的元素具有相似的外观和行为,从而提高用户体验和用户界面的一致性。
※ 高性能和丰富社区:
相比于传统的 CSS 框架或预处理器,Tailwind CSS 的学习曲线相对较低。由于它采用了原子类的概念,开发人员不需要记忆复杂的命名规则或层叠样式表的优先级,只需根据需要选择合适的类名即可。
Tailwind CSS 通过优化样式表的生成方式,可以生成高效的 CSS 代码。在构建过程中,Tailwind CSS 会根据项目实际使用的原子类来生成最终的样式表,避免了传统 CSS 框架中可能出现的未使用样式的冗余。
Tailwind CSS 还拥有庞大的社区支持和活跃的开发团队。你可以在社区中找到大量的教程、文档和插件,以及与其他开发人员交流和分享经验。
局限性及应对策略
※ 学习曲线与类名记忆
开发者需要掌握 200+ 核心工具类命名规则:
示例: text-lg (大号文字) vs text-xl (超大文字)
- 使用VSCODE Tailwind IntelliSense 插件实现自动补全,同时hover到class上的时候会显示具体的样式值。
※ HTML 可读性下降
当元素上CSS样式过多时,会导致html的可读性下降,一般情况下尤其是还存在响应式等样式的时候。
复杂组件示例:
<button class="flex items-center justify-center px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 disabled:opacity-50" > 提交订单 </button>
优化策略:
- 提取组件,结合 @apply 指令,将多个原子样式组合成新的样式类。
// 提取组件 + 抽象语义 <div class="btn-primary">提交订单</div>
// 结合 @apply 指令 .btn-primary { @apply flex items-center justify-center px-6 py-3 bg-blue-500 text-white rounded-lg; }
※ 深度定制场景成本
当设计系统需要突破默认约束时,Tailwind 允许通过 tailwind.config.js 文件进行自定义配置,例如如下需要拓展间距相关的CSS熟悉时:
// tailwind.config.js // 需要扩展非标值 theme: { extend: { spacing: { '128': '32rem', '13': '3.25rem' // 违反默认进制规则 } } }
// 使用<div class="w-128 p-13">新的 Spacing 规则<div>
可能会破坏原子化一致性原则。
最佳实践:
- 尽量遵循默认约束体系 。
- 通过 CSS 变量注入特殊值:
<div class="w-[327px]"></div> <!-- 临时解决方案 -->
UnoCSS
UnoCSS 是一个高性能且高度灵活的原子化 CSS 引擎,由 Vite 核心团队成员 Anthony Fu** 开发。它的核心理念是“按需生成原子类”,以极快的构建速度和极简的配置为特点,成为现代 Web 开发中 Tailwind CSS 的强力替代品。
使用流程
※ 安装依赖
# 使用 npm npm install -D unocss
# 使用 yarn yarn add -D unocss
# 使用 pnpm pnpm add -D unocss
※ 框架集成
// vite.config.tsimport UnoCSS from 'unocss/vite'
export default { plugins: [UnoCSS()] }
// main.js(注入运行时) import 'virtual:uno.css'
※ 核心配置解析
创建 uno.config.ts 实现深度定制:
// uno.config.ts import { defineConfig, presetUno } from 'unocss'
export default defineConfig({ content: { filesystem: [ './src/**/*.{html,js,ts,jsx,tsx,vue}', './packages/**/*.{html,js,ts,jsx,tsx,vue}' ] }, // 预设系统(必选) presets: [ presetUno(), // 核心原子类 presetAttributify(), // 属性化模式支持 presetIcons(), // 图标系统集成 ], // 自定义规则 rules: [ // 动态间距规则 [/^space-(\d+)$/, ([, d]) => ({ 'margin-inline': `${d * 4}px` })], // 自定义颜色系统 [/^c-(red|blue|green)$/, ([, c]) => ({ color: `var(--color-${c})` })], ], // 快捷方式 shortcuts: { 'btn': 'px-4 py-2 rounded bg-blue-500 text-white', 'flex-center': 'flex justify-center items-center', }, // 主题系统 theme: { colors: { primary: '#01c2c3', danger: '#ef4444' } } })
※ 原子类使用实战
- 基础用法:
<!-- 传统类名模式 --> <div class="m-4 p-2 flex items-center"> <div class="w-1/2 h-[200px] bg-#BADA55"></div> </div>
<!-- 属性化模式(需 presetAttributify 插件) --> <div m="4" p="2" flex items-center> <div w="1/2" h="200px" bg="#BADA55"></div> </div>
- 响应式与状态:
<!-- 断点系统 --> <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3"></div>
<!-- 悬停/焦点状态 --> <button class="bg-blue-500 hover:bg-blue-600 focus:ring-2"></button>
<!-- 深色模式 --> <div class="bg-white dark:bg-gray-800"></div>
- 动态生成:
<!-- 任意值支持 --> <div class="w-[calc(100%_-_32px)]"></div>
<!-- 组合指令 --> <div class="grid-cols-[repeat(auto-fit,minmax(200px,1fr))]"></div>
※ 高级功能解锁
- 图标系统集成:
// 安装图标引擎 npm install -D @unocss/preset-icons @iconify/json
// 配置 presets: [ presetIcons({ scale: 1.2, extraProperties: { 'display': 'inline-block', 'vertical-align': 'middle', }, }) ]
// 使用 <div class="i-mdi-alarm text-red-500"></div>
- CSS 层级控制:
<div class="[&>:nth-child(3)]:text-red-500"> <div>Item 1</div> <div>Item 2</div> <div>Item 3(将变红)</div> </div>
- 动画系统:
// 配置自定义动画 theme: { animation: { keyframes: { 'fade-in': '{0% {opacity:0} 100% {opacity:1}}' }, durations: { 'fade-in': '0.5s' } } }
// 使用 <div class="animate-fade-in"></div>
对比 Tailwind CSS 的范式优势
※ 生成策略的本质差异
※ 配置系统的自由度
- Tailwind 的约束配置:
// 只能扩展预设主题 theme: { extend: { spacing: { '128': '32rem' } } }
- UnoCSS 的开放规则:
// 可完全重写规则体系 rules: [ [/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` }), [/^p-(\d+)$/, ([, d]) => ({ padding: `${d}px` }) ]
※ 跨框架的原生支持
当前局限性
※ 规则冲突的调试成本
// 多个正则规则可能冲突 rules: [ [/^m-(\d+)$/, ([, d]) => ({ margin: `${d}px` }) , [/^m-(\d+)-(\d+)$/, ([, x, y]) => ({ margin: `${x}px ${y}px` }) ] // 输入 m-4-2 可能触发错误匹配
解决方案:
- 精确正则约束(如 ^m-(\d+)-(\d+)$ )。
- 使用 enforce: 'pre' 调整规则优先级 。
※ 生态工具链成熟度
※ 团队协作规范压力
- 自由度过高的风险场景:
<!-- 开发者A写法 --> <div class="flex items-center"></div>
<!-- 开发者B写法 --> <div flex items-center></div>
<!-- 开发者C写法 --> <div style="display:flex; align-items:center"></div>
最佳实践:
- 使用 Biomejs 规则限制写法统一
- 制定《UnoCSS 团队规范白皮书》
- 通过预设强制约束:
// 禁用原生 style blocklist: [/style=".*"/]
五、总结
在 Web 开发中,无论是纯手写 CSS、采用工程化方案(如 Sass、CSS Modules),还是直接使用原子化 CSS 框架(如 Tailwind、UnoCSS),其核心目标始终围绕效率与质量的平衡。通过提高代码复用率、减少冗余逻辑、统一设计规范,开发者能够避免重复造轮子的时间损耗,同时降低维护成本。手写 CSS 追求极致的灵活性与语义化,适合对样式控制要求极高的小型项目;工程化工具通过变量、嵌套和模块化机制,为复杂系统提供结构化解决方案;而原子化框架则以“组合优先”的实用主义,将样式拆解为可复用的颗粒化单元,尤其契合快速迭代和团队协作的场景。值得注意的是,代码的可读性与风格统一性始终是技术选型的关键考量——杂乱无章的类名堆砌或过度抽象的样式封装,都可能成为长期维护的隐患。因此,开发者需根据项目规模、团队习惯与设计诉求灵活抉择:若钟爱原子化框架的即时反馈与设计约束,便不必囿于传统 CSS 的“纯净性”;若追求语义化与动态样式的深度控制,亦可拥抱工程化工具的强大扩展能力。技术终为手段而非目的,唯有匹配实际需求与个人心智模型的高效实践,才能在代码的严谨性与开发的愉悦感之间找到最优解。
往期回顾
2.以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
3.得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 三七
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
AI生成功能设计用例|得物技术
一、AI背景
人工智能生成内容(AIGC,AI-Generated Content)技术的快速发展正在改变内容生产的方式,并逐渐渗透到各个行业,例如:在自媒体平台自动编写文案并发布,快速分析数据,写小说,画漫画等。强大的文本生成能力已经实现了生产力超过生产资料,提供了更加高效的生产力,将AI引入到工作中成为发展的方向。
目前公司编写测试用例为人工编写,存在手工编写用例的普遍痛点,例如:重新编写,费时费力,边界遗漏,兼容遗漏等。AI拥有自动生成文本并快速整合的能力,以AI辅助功能用例编写成为推动行业创新和效率提升的关键点。
AI编写用例的优点:
※ 效率提升
AI可以快速生成大量测试用例,显著减少人工编写所需的时间,提升整体测试效率。
※ 测试覆盖提升
AI能够自动识别潜在的测试场景和边界条件,从而提高测试覆盖率**,确保更全面的检测。
※ 一致性和准确性提升
AI生成测试用例具有较高的一致性和易理解性,减少人为错误,增强测试的可靠性和准确性。
AI热词:
二、设计方案
本部分介绍使用AI编写测试用例的的设计方案,包括使用流程和架构图。
AI编写用例流程图
AI编写用例架构图
三、设计核心介绍
本部分介绍如何使用AI辅助生成功能用例,详细讲解了从PRD文档->测试点->测试用例->Xmind用例->使用采纳,整条链路的核心设计与实现。
PRD文件解析器
平台支持飞书PRD文档中文本、多维表格、电子表格内容的解析,暂不支持对图片、流程图解析。文档读取分为6个步骤,分别为:获取飞书token、获取用户token、获取文件block列表、Table表格解析、电子表格解析、解析结果组装。以下主要介绍解析部分内容:
结构组成设计:
实现方案详情
※ 飞书文档读取
※ Table的提取与sheet表格的提取
- Table提取:提取表格过程中需要将表格相关的块与子块关联绑定,递归解析所有的数据。并根据第一行各字段的长度<20做是否为表头判定,默认第一行为表头信息。
- sheet提取:在飞书表格提取过程中需要使用多个递归,分别获取表格所有内容与元素
※ AI解析PRD文档:
- PRD解析:通过与AI交互将文本内容解析为:需求关键字、测试背景、测试需求详情三部分,并按照特定字段将数据存储。
- 结构设计:
PRD解析结构设计
核心代码逻辑:
※ 获取关联测试需求业务背景:
- 根据PRD解析关键字信息匹配最相关的测试用例模块,使用向量和关键字双权重对RAG**模块做测试用例提取:
- keyword_weight:0.3
- vector_weight:0.7
- 同时设置AI模型准确度为0.85
- 匹配过程中分别针对不同的关键字,从RAG数据中提取热度最高的3个测试模块,合并后提取所有模块中热度最高的三个模块作为业务历史背景。
- RAG提取架构设计
- 核心代码逻辑
模型设计
测试点生成器
测试点生成器为AI生成用例的核心,实现PRD到测试点的转换。生成过程中结合需求背景、关键字、需求详情、业务背景、测试分析等信息作为业务背景,以更准确的生成测试用例。核心结构如下:
结构组成设计
实现方案详情
模型设计
测试用例生成器
测试用例生成器为AI用例生成器,负责将AI测试点转换为Xmind测试用例,主要实现两个功能,第一步将AI测试点转换为markdown结构的测试用例,包括用例名称、前置条件、执行步骤、期望结果等。第二部负责将第一步测试用例转换为Xmind结构。
实现方案详情
※ 测试点解析生成markdown格式用例:
生成markdown格式用例
解析结果
※ AI markdown格式转换为Xmind结构用例
转换Xmind结构
生成结果
模型设计
知识库搭建
LLM大模型有通用的推荐能力,针对公司业务场景是无法准确识别相关功能的,针对“最后一公里”问题,平台使用搭建测试用例知识库的方式,以提升推荐准确度。
平台会以历史测试用例与业务需求文档作为历史业务背景。在推荐功能用例过程中自动匹配历史业务背景,以提升推荐准确度。
知识库搭建
※ 知识库涉及范围
※ 实现方案详情
- Xmind测试用例转换知识库
- 业务文档转换知识库
※ 模型设计:
- 测试用例转换文本AI模型
- 业务文档转换业务文档模型
四、实现结果展示
五、总结 & 规划
目前平台侧已经实现自动生成功能用例的功能,实现了从 PRD自动解析->测试点生成-> Xmind用例生成->同步平台的完整流程。可以一定程度上提升用户编写用例效率。
后续规划
- 支持PRD文档图片/流程图等多模态数据解析
- 持续完善RAG模型与测试用例知识库的维护
往期回顾
2.以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
3.得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 执一
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
从零实现模块级代码影响面分析方案|得物技术
一、名词解释
代码影响面(Code Impact Analysis)
是指在代码变更后,分析这些变更对系统中其他部分的影响范围。它帮助开发团队理解代码修改的潜在影响,从而减少意外问题并提高代码质量。
模块级
是指以模块(Module)为单位的代码组织、分析和管理的粒度。模块是代码的基本单元,通常包含一组相关的功能,可以是 JavaScript 文件、UI 组件、页面或其他功能单元。
二、背景 & 价值
在过往交易域稳定性建设中,我们完成了多项关键工作,包括后台应用拆分、历史债务重构、权限配置管控和核心H5页面定期巡检任务等。此外,我们还整合了前端监控平台的各类异常数据分析与告警能力,帮助提前发现系统性风险,以提升系统的整体稳定性。
通过对于以往故障案例的复盘,我们也识别出一些导致系统稳定性问题的潜在隐患,尤其是随着业务复杂度提升,单个版本往往涉及大量页面改动和复杂的依赖关系。现有的影响面评估方式难以全面覆盖这些变更,在这种情况下容易导致出现生产问题时止血时间的拉长,影响了系统的稳定性和用户体验。
在迭代发布视角下,代码影响面的分析尤为重要。每次迭代发布通常涉及多个功能或模块的更新,而这些更新可能会对系统的其他部分产生直接或间接的影响。
问题梳理
风险评估滞后
依赖人工经验判断改动影响面,在涉及多人协作和多个模块的团队开发或Monorepo等复杂场景下尤其低效。
信息维度割裂
现有研发协同平台以需求为纬度聚合研发相关信息,而前端稳定性保障则更需要以页面为纬度聚合迭代相关信息。
变更追踪困难
关键变更信息散落在群聊或各个系统中,缺乏一个统一的平台来聚合这些信息,导致信息同步和协作效率低下。
因此,我们希望实现一套自动化收集模块级代码影响面分析的方案,并以此评估版本需求发布对于系统整体稳定性的影响,从而提前确保重点模块能够得到有效的预警和监控,并创建相应的预案计划。
价值收益
研发自测能力提升
能够更精准地识别更改影响的页面或模块,确保需求影响范围符合预期。
测试覆盖率优化
结合变更影响,确保关键路径的完整测试,提升测试的有效性和覆盖率。
评估系统复杂度
有助于全面评估版本发布影响面范围;对系统各业务模块进行合理资源分配。
三、技术方案
代码影响面分析的完整方案分为多个关键步骤,通过这些步骤可以实现自动化收集模块级代码影响面分析,并评估版本需求发布对系统整体稳定性的影响。
具体可以参考下面的流程图了解👇:
详细设计
影响面分析引擎
通过结合代码变更、依赖关系、业务逻辑等多维度数据,帮助开发团队快速识别和评估代码修改的潜在影响,从而减少生产问题的发生,提升系统的稳定性和代码质量。
※ 依赖关系图构建
- 使用静态分析工具分析项目中模块的依赖关系
- 根据项目类型分别构建依赖关系图
- 展示变更模块对其他模块的影响路径
※ 代码变更分析
- 使用版本对比工具分析代码变更
- 基于DIFF数据,统计变更的函数和变量
- 根据依赖关系图,初步分析变更的影响范围
※ 影响范围标记
- 从变更点出发,追踪调用路径,标记所有受影响的节点
- 将影响范围分为模块、功能、接口和数据四类
- 解析文件路由信息,输出页面列表
根据简化后的代码,可以快速理解核心功能的实现原理。
class CodeEffectAnalyzer { private fileImports: { [key: string]: FileImport[] };
// 收集文件的导入依赖 private collectImports(filePath: string, ast: any): void { traverse(ast, { ImportDeclaration: ({ node }) => { // 记录导入关系 node.specifiers.forEach((specifier) => { this.fileImports[filePath].push({ filePath: path.resolve(path.dirname(filePath), node.source.value), importedName: specifier.imported.name, localName: specifier.local.name, }); }); }, }); }
// 分析文件,提取导出变量和函数 private analyzeFile(filePath: string): FileDetails { const exports: FileExports = {};
// 遍历 AST,提取导出项 traverse(ast, { ExportDefaultDeclaration: (path) => { exports['default'] = generate(path.node).code; }, ExportNamedDeclaration: (path) => { const declaration = path.node.declaration; exports[declaration.id.name] = generate(path.node).code; }, }); return { exports }; } // 影响面分析检索 public analyzeImpact(affectedFiles: string[]): AffectedResult { const analyzeImpactRecursive = (filePath: string): void => { const { exports } = this.analyzeFile(filePath); const modifiedList = Object.keys(exports); // 假设所有导出项都被修改 const referencedList: string[] = []; // 找出引用了修改项的代码 for (const imported of this.fileImports[filePath] || []) { if (modifiedList.includes(imported.importedName)) { referencedList.push(imported.localName); analyzeImpactRecursive(imported.filePath); // 递归分析影响面 } } }; // 分析每个受影响文件 for (const file of affectedFiles) { analyzeImpactRecursive(file); } }}
平台数据聚合
在各个系统平台之间实现系统稳定性数据的一致性和实时更新,以确保各个部分能够获取最新的、准确的信息,进一步实现高效协作和准确分析。
※ 天网权限系统对接
- 获取菜单层级结构和页面路径信息,支持功能权限配置校验
- 数据扁平化转换,微前端场景下提取子应用标识
※ 研发协同平台同步
- 获取迭代需求效能数据,进行汇总与计算
- 建立需求任务与代码模块的关联
※ 前端监控平台集成
- 获取页面性能指标(首屏加载时间-FCP、接口响应耗时)、异常数据(JS异常数、接口成功率)以及流量数据(页面访问量-PV、页面访问数-UV)
- 数据清洗工作(异常值过滤、重复数据移除),数据格式标准化
结果信息可视化
将代码变更的影响范围以直观、易懂的图形或图表形式展示出来,并嵌入研发生命周期,帮助开发团队快速理解变更的潜在影响,并做出相应的决策。
※ 使用可视化工具
- 通过图形化界面直观展示代码变更的影响范围,降低理解门槛
- 交互联动,点击不同模块直接跳转至关联的平台详情页
※ 生成多维报告
- 从多个核心维度分析影响面指标
- 提供各维度的分析数据填充至报告模版
※ 集成 CI/CD 流程
- 在合并请求(MR)阶段触发影响面分析并生成报告
- 同时支持手动创建影响面分析任务
数据库设计
根据架构方案设计,规划出如下四个表数据结构,用来存储发布应用数据、影响面结果数据、页面异常/性能数据、研发效能等信息,支持高效查询和扩展性。
业务效果
迭代发布对系统整体的影响是多维度的,从不同视角进行发布影响面的全面评估,可以协助责任人制定发布重点监控方向,从而有效减少风险。
按人员类型划分成不同角色视角
※ 测试视角
※ 研发视角
※ 管理视角
按影响面维度划分成多个展示效果
※ 任务详情
※ 模块列表
※ 接口信息
※ 需求信息
四、挑战 & 优化
在大型项目中,模块间的依赖关系复杂,如何高效、准确地构建依赖关系图是一个挑战。
挑战1:复杂依赖关系分析
※ 问题描述
- 代码风格与框架差异。 不同项目采用不同技术栈、模块化方案、动态语法及特殊语法导致解析困难重重
- 动态依赖难以追踪。 运行时依赖(如按需加载、环境变量分支逻辑)无法通过静态分析捕获
- 系统路由规则差异。 不同系统采用不同的路由方案,其中微前端场景下,主应用与子应用的路由可能独立管理,形成多层嵌套路由结构
※ 解决思路
- 多语言/框架适配。 统一AST解析引擎,兼容主流模块化规范
- 运行时依赖追踪。 选择动态分析工具并添加日志记录
- 统一路由元信息提取。 多框架路由解析适配器,微前端主子应用路由协同
挑战2:跨内部平台系统集成
※ 问题描述
- 接入流程繁琐。 各内部平台系统需单独申请权限配置令牌,重复操作多,维护成本高
- 数据实时性与一致性。各平台数据更新频率不同,聚合时可能产生冲突
※ 解决思路
- 模块化设计架构。 功能模块独立开发,优先级划分,MVP思维
- 数据版本快照。 版本控制管理,对关键数据人工干预兜底
优化1:跳过额外分析检测
在CI/CD流程中,部分代码变更(如文档更新、配置文件调整)无需触发完整的代码影响面分析。通过检测机制,可减少不必要的资源消耗,提升流水线执行效率。
- 条件判断跳过分析。 根据变更文件类型或所在目录信息,动态决定是否执行分析
- 提交信息比对。 比较两次检测之间的 commit** 差异,无内容主动跳过分析
- 白名单机制。 对特定文件或目录配置白名单,包含无需分析的特定文件或目录
优化2:缓存机制优化
合理的缓存策略和异步任务处理可以优化检测效率,降低 CPU 使用率和内存占用,进而提升系统整体性能。
- 设置适当的缓存失效策略。 以模块或文件的唯一标识(如文件路径、Git提交哈希)作为缓存键,当依赖项或代码发生变更时,清空相关缓存
- 任务异步处理。 将依赖分析和 AST 解析任务异步处理,使用消息队列将任务排入队列,避免阻塞主线程
五、总结展望
通过实现一套自动化收集模块级代码影响面分析的方案,我们可以更精准地评估版本需求发布对于系统整体稳定性的影响,从而提前确保重点模块能够得到有效的预警和监控,并创建相应的预案计划。这将有助于提升研发自测能力、优化测试覆盖率、评估系统复杂度,最终提高系统的稳定性和代码质量。
之后我们将继续优化影响面分析引擎,提升依赖关系分析的准确性和效率,进一步融合多维度数据,完成在线流量报表、全栈大盘数据建设,实现更高效的数据聚合和可视化展示,为开发团队提供更强大的支持。
往期回顾
2. 以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
3. 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 卓翎
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
得物自研DSearch3.0搜索核心引擎升级之路
一、背景
随着交易和社区搜索业务稳步快跑,基建侧引擎越来越复杂,之前搜索底层索引查询结构已经存在较为严重的性能瓶颈。成本和运维难度越来越高。在开发效率上和引擎的稳定性上,也暴露出了很多需要解决的运维稳定性和开发效率短板。而在引擎的业务层部分也需要逐步升级,来解决当前引擎中召回层和业务层中各个模块强耦合,难维护,迭代效率低下等问题。
二、引擎开发技术方案
DSearch1.0索引层整体结构
DSearch1.0的索引结构比较特殊一些,总体上使用了全局rcu的设计思想,整体架构上单写多读,所以实现了并发高性能无锁读,内部数据结构都是无锁数据结构,所以查询性能高。在写操作上因为rcu机制实现写入无锁。整体上优点读性能高,没有传统段合并操作带来的磁盘抖动。缺点是索引地址和操作系统强相关,运维复杂,热更新受限。全局地址分配难以并行写入,构建瓶颈明显。无法对浪费的内存进行回收导致内存空间利用率低,索引空间占用大。总体结构如图所示:
DSearch2.0的索引升级
DSearch2.0分段索引整体设计
引擎2.0索引升级采用经典段合并架构,除了继承了段合并中优异的高性能写入性能和查询已经索引合并等优势外,针对段合并中频繁的正排字段更新等带来的高IO缺点。我们设计了新的正排字段原地更新索引,使新的DSearch2.0引擎拥有Redis的高性能写入和查询,也拥有lucene的紧凑索引和索引合并带来的内存空间节省的优势。
※ 索引段结构
- 每个索引段包含了文档文件,用于紧凑存放document中的各个字段的详细信息。字符串池文件是对document中所有的字符串进行统一顺序存储,同时对字符串进行ID化,每个字符串ID就是对应于字符串池中的offset偏移。
- 可变数组文件是专门存放数组类型的数据,紧凑型连续存放,当字段更新的时候采用文件追加append进行写。最终内存回收通过段之间的compaction进行。FST索引文件是专门存放document中全部字符串索引。每个fst的node节点存放了该字符串在字符串池中的偏移offset。而通过字符串的offset,能够快速在倒排termoffset数组上二分查找定位到term的倒排链。
- 倒排文件是专门存放倒排docid,词频信息、位置信息等倒排信息,其中docid倒排链数据结构会根据生成段的时候计算docid和总doc数的密度来做具体判断,如果密度高于一定阈值就会使用bitmap数据结构,如果小于一定阈值会使用array的数据结构。
- 标记删除delete链主要是用于记录段中被删除的document,删除操作是软删除,在最后查询逻辑操作的时候进行最后的过滤。
- 实时增量的trie树结构,实时增量段中的前缀检索和静态段中的前缀检索数据结构不一样,trie因为能够进行实时更新所以在内存中使用trie树。
- 段中的metadata文件,metadata文件是记录每个段中的核心数据的地方,主要记录段内doc数量,段内delete文档比例,实时段的metadata会记录kafka的offset等核心数据。
Document文档和索引结构
※ Document文档数据结构
- Document文档使用紧凑型存储,其中array和字符串类型单独存放,其他字段连续存放,string和array字段存放。
- array字段类型数据直接存放在可变数组文件区,连续追加写。
- string字符串池对所有字符串进行连续存放,多个doc中同一个字符串引用同一个字符串地址,节省大量字符串存放空间。
※ 倒排索引文件结构
- 倒排索引文件存放docid倒排和Tf以及位置position数据。其中内存实时段中的倒排索引数据结构是固定一种类型array类型。而内存实时段固化为静态段的时候,倒排数据结构会根据docid中的密度进行选择array和bitmap存储。当docid密度大于一定阈值是bitmap,反之是array结构。
- Tf数据结构是一个uint16的数组,数组长度和docid的数组长度一致,所以当确定了某个docid时候,也随即确定了它的tf信息。
- postion信息存储是一个二维数组的格式,第一层数组存放的是对应于term的在字符串池的offset,因为term在字符串池中已经ID化,所以offset可以表示唯一term。第二层数组是该term在字段中多次出现的位置,使用uint16存储。
※ 前缀检索文件
-
FST静态段文件
a. 静态段中前缀是fst的数据结构,因为fst一旦建立是不能够进行修改的,所以在段合并的时候需要对所有term进行排序然后再构建fst结构。
b. fst的node节点存放了对应于term的字符串池的offset。当需要查询一个term的倒排结构时候,需要先查询该term的字符串池的offset,然后拿该offset去倒排的termoffset文件中二分查找找到对应的倒排positionlist结构拿到对应倒排。所以一次term到倒排的查询需要查询一次fst+一次二分查询。
c. term到倒排的查询一次fst+一次二分查找效率不高,所以针对term到倒排查询,新增了第二种HashMap索引,直接通过term到倒排的offset索引,这个选项在建表的时候可以配置。
-
实时段RcuTrie树索引
a. 实时段中需要支持边写边读,前缀检索需要支持并发读写。引擎中trie树是rcu实现,单线程更新,多线程并发读,trie树写更新节点内存延迟回收。
倒排索引和查询树逻辑
※ 倒排链优化
- DSearch1.0的roaringbimap倒排索引在低密度数据量上存在一些瓶颈,比如对于倒排链比较短的情况下,roaringbitmap的container大部分都是array结构,在倒排链查询和合并都会进行一次二分查找,在大面积的倒排链合并中是个相当大的性能瓶颈。
- 针对上面所说的情况对roaringbitmap进行了精简,只存array或者bitmap合并的时候不需要查找,直接链式合并。
※ 逻辑树合并优化
- DSearch2.0重点从逻辑语法树和倒排入手,优化语法树,减少合并树高,从二叉树合并变成单层合并。
- 优化倒排链合并方式,采用原地倒排链合并,消除倒排合并临时对象,同时引入多线程并行合并,减少长尾提高性能。
增量更新逻辑
※ 增量实时写入逻辑
- 引擎支持多个并发实时段,这个由配置文件通过配置来进行配置。多个实时段能够提升并发写入的性能。
- 每个实时段对应一个写入队列,提高并发写入吞吐。
- 每个段真实写入一条信息会同步原子更新消费的kafka的offset,用于对后面进程重启等恢复数据做准备。
- 当进程重启或者异常退出时候,会读取metadata文件中的最后一条kafka offset进行重新消费增量在内存中重新构建新的正排、文档和倒排等信息,完成数据的恢复。
实时段固化和段合并策略
※ 实时段固化逻辑:
- 当实时段内随着增量写,doc文件大小超过128M时候会进行内存实时段固化操作。
- 固化操作开始时,会先生成新的内存实时段,老的内存实时段会变成只读内存段。
- 遍历按整个只读内存段,构建新的索引和新的正排结构生成新的静态段。
※ 段合并策略:
- 实时段固化的小静态段因为大小比较小,会优先和之前固化后的小段进行合并,按照1,2,4,8进行合并,逐步合并成静态段最大的上限。
- 静态段的合并触发策略是当静态段中delete的doc比例超过了30%会触发静态段之间的合并,合并会按照近邻合并原则,从左右近邻中选取一个最小doc数的段进行合并,进而新生成一个新的段。
查询和更新中的并发控制
※ 查询流程
引擎查询时候,先遍历查询实时段,然后再查询静态段。实时段查询存在最大增量查询截断,当实时段查询到最大增量截断时实时段停止查询。
实时段查询后,查询静态段。静态段中包含了全量构建索引的全量最大offset记录同时全量的doc是通过质量分进行排序,所以在全量段查询的时候,先遍历质量分最大的全量段,逐步往后面静态段查询,直到查询到全量截断。
实时段查询和静态段查询结果进行merge作为最终的查询结果。
※ 更新并发控制
因为DSearch2.0的索引更新是直接在实时段或者静态段进行更新,所以存在多线程读写问题。尤其是正排字段更新写入量大更新频繁。同时更新涉及到所有的实时段和静态段,较为复杂。
为了解决正排字段和倒排的更新问题,新版本引擎引入了document文档锁池,对每个doc进行hash计算落到锁池中具体一个锁上来减少锁冲突,当前锁池内有多个个文档锁。文档锁在文档进行拷贝和更新的时候会进行锁住。
DSearch3.0搜索核心升级
异步非阻塞图调度框架
※ 引擎主要改造:
- 图框架支持RPC异步非阻塞请求:引擎图框架RpcServer服务使用brpc的异步处理无需同步阻塞等待调度完成,只需框架调度完算子返回结果,不阻塞RpcServer线程,例如:当前引擎调用neuron服务是同步调用,当neuron服务负载高阻塞时,同步调用会导致拖住引擎RpcServer处理线程,新的异步非阻塞模式引擎client在调用引擎后已经返回,等待引擎RpcServer中异步调度框架中remote异步算子回调,减少外部服务影响引擎。
- 减少线程切换: 图框架调度器会优先调度当前运行线程,同时使用M:N类型的bthread线程池,线程切换会更小,执行效率高。
- RPC服务和框架算子独立: 引擎RPC服务和框架算子完全解耦,跨集群部署算子服务无需任何改造,实现算子脱离运行环境。
- 高效的算子异常处理和超时机制: 每个算子维护自己的运行超时时间和请求到算子调度执行的超时时间,对整个请求流程中各算子执行更加精准。
- 动态图支持: 图框架支持静态图和动态图业务组合式调用。支持静态子图和动态子图调用等复杂业务组合。
- 复杂子图支持: 图框架支持嵌套子图,支持自调用模型,可以实现复杂单节点多功能调用。
算子间数据交换Table设计
※ 引擎主要改造:
- 列式数据共享优化: 算子交换数据全部存放在Table列中,Table中全部共享列式数据,省去大面积数据拷贝,大幅提升引擎业务执行性能。
- 兼容引擎索引中doc数据: 引擎索引中doc行式存储有很多优点,比如多字段访问效率高等,Table设计中考虑了行式存储优点,不仅存高频的列字段也储存了引擎内部的doc以及对应FieldDef,能直接方便访问索引数据,接口统一,易于迭代。
- 打通FlatBuffer序列化协议: 当前引擎FlatBuffer序列化传输协议和引擎内部数据出口需要多次遍历转换,需要拷贝很多数据,新Table的设计内部数据列和FlatBuffer内部的数据列互转互通,节省大量内部拷贝同时避免了字段兼容等问题。
- 支持原地排序和标记删除: Table数据表,支持原地sort操作和标记删除操作,节省数据排序时大量数据的拷贝和删除操作中导致的数据重排等拷贝操作,提升性能。
算子间数据交换Table设计
※ 引擎主要改造:
- 动态图支持: DSsearch3.0支持动态图编排,主要通过业务方通过动态编排请求来组织对应的算子编排逻辑,实现业务方自主编排调度逻辑,方便整体业务开发。
- Remote远程调用支持: 通过开发远程异步调用算子,支持DSearch3.0跨集群调用,实现多机算子化互联互通。提高引擎的整体纵向拓展能力。
- 引擎算子库复用: 通过设计统一的算子接口,开发基础的可复用框架算子,支持配置化组合运行图,实现业务逻辑快速复用和开发,提高整体引擎开发效率。
三、性能和效果提升
DSearch在2024年Q1季度索引升级开发完成后逐步推全到交易和社区等各个主场景业务中,最后拿到了很多超预期结果:
索引内存优化超出预期: 社区搜索和交易搜索总索引单分片优化60%。
构建和写入性能优化超出预期: 社区搜索和交易搜索主表写入性能提升10倍。
索引更新优化超预期: 社区和交易主表更新时间提升接近10倍。
性能优化符合预期: 社区搜索平均rt降低一倍,P99晚高峰降低2倍。
四、总结
DSearch引擎从开始的DSearch1.0的搜索引擎逐步经历了DSearch2.0的分段式索引改造升级,又经历了DSearch3.0的全图化引擎升级。逐步将DSearch引擎升级到业界较为领先的支持内存型、磁盘型多段式搜索引擎,为支持得物业务的发展做出了重要的贡献,后续DSearch会围绕着通用化、自迭代、高性能等多个方向继续升级,将DSearch引擎迭代到业界领先的引擎。
算法团队大量HC,欢迎加入我们:得物技术大量算法岗位多地上线,“职”等你来!
往期回顾
1. 以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
2. 最近爆火的MCP究竟有多大魅力?MCP开发初体验|得物技术
3. 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 苏黎
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
以细节诠释专业,用成长定义价值——对话@孟同学 |得物技术
一、前言
在得物技术部,「稳定」「效率」「体验」「成长」「创新」是我们的关键词。这些关键词就像是战略航行的导航系统:在短期诱惑前构筑认知屏障,筛选干扰项;在组织进化中沉淀文化基因,保持创新。其中的「成长」就意味着专业深耕中永不自满的自我迭代、跨边界协作中主动打破能力天花板的勇气,以及在成就业务目标的同时构建个人价值护城河的清醒认知。
作为得物技术保障部的容器技术团队成员,孟同学在入职两年内迅速成长为团队标杆人物,其主导的【一站式大模型训练与推理平台项目】 不仅极大降低了大模型接入成本,在社区、客服、公司内部应用等场景成功落地,增强了业务价值与用户体验。在公司内外多次积极分享技术成果,提升了公司技术影响力,更以极致细节与自驱力在内部形成示范效应。
正值Q2成长宣传季,技术运营牵头做人物采访,本季度将会采访两位在得物成长比较快的同学,看看他们究竟做了什么?又是如何将「成长」「自驱」融入工作中?今天我们来看看第一位同学「孟同学」,看看他背后的故事。
二、初心与选择:得物的创新很吸引我
孟同学之前在多家互联网公司工作,包括腾讯、Paypal、唯品会、蚂蚁,2019年后阿里达摩院从事算法工程开发;2022年10月加入得物,在得物容器技术从事算法工程相关工作,主要负责得物大模型平台的相关业务。
当时,得物发布了一个云原生AI的职位,要求既有云原生技术背景,又能涉猎AI领域。这两个方向在都是业界的热门趋势,一时间挺难招到比较合适的人,这时候孟同学出现了。他正好在阿里达摩院从事类似的工作,且具备一定的云原生与AI的经验。他说:业界大多数岗位通常会专注于云原生或AI某一方向,但得物把这两者结合起来招聘,给了他一个新的视角和机会。于是他就抱着试试的心态来了得物。
他说:“这样的职位可以让我在专业技术上做一些新的探索,尝试将云原生与AI融合,可能带来更多创新的空间。带着“试试看”的心态,我投递了得物的这个职位,最终决定加入得物,去应对这个充满挑战和机遇的新环境。”
加入得物后,得物的文化和他也超级契合,他说,“在文化价值观中,最吸引我的是得物对 “效率”和“创新” 的高度重视,在当前快速发展的科技环境中,得物不仅倡导快速迭代,还鼓励在保证高效执行的基础上,持续创新并不断突破常规。我现在所在的容器团队就深刻体现了这一点。我们的工作模式通常是先通过小范围验证,快速实验新技术或方案的可行性,确认其技术路径可行且能够带来预期收益后,再进入大规模的开发与应用。这样整个容器团队紧跟技术发展,高效上线了很多新的好的技术优化方案。”
他也是得物技术飞速成长的员工之一,当技术运营问到他如何理解「成长」这一关键词,他表示, “成长”是一个持续自我突破和不断提升的过程。在快速变化的环境中,技术人员不仅要不断提升专业能力,还需要敢于走出舒适区,迎接新的技术挑战。成长不仅体现在技术上,更是一种心态,保持持续学习和反思的能力,追求更高标准。
他在主导“一站式大模型推理平台项目”时,面临的最大挑战是如何降低大模型接入的高成本。2023年初,刚开始接入大模型时,由于推理引擎性能较低,需要大量GPU卡,很多业务难以落地。如果按常规思路,这个项目很难推进。既然常规思路走不通,那么他就换了个思路,通过主动关注社区的最新论文和开源代码**,去尝试社区中提到的优化技巧和加速方案。通过把社区的创新思路与得物内部场景结合,快速验证并落地到得物内部的大模型推理平台中,将优化方案应用到实际场景中,降低了成本并提升了平台性能。
Sean曾在π问π答说过:成长 = 当你遇到复杂问题的时候,能解决复杂问题。 就是说,在这个过程中,克服困难,牵头思考解决方案、寻找解决方案,这样的成长是最快的。回顾孟同学的成长过程,亦是如此。
三、专业与细节:细节直接影响整体效果
在从0到1 建设一站式大模型训练推理平台时,孟同学也遇到了很多问题。包括做这件事的 ROI 是什么?为什么要做这个项目?怎么落地?需要谁来协同?
ROI是什么?
从大模型在业内的落地情况来看,大模型相比传统小模型在效果上有显著提升,尤其在处理复杂任务,的确很有价值。但是从一个开源的大模型到能落地到我们的实际业务场景,需要投入较大的人力资源、研发周期、机器成本等。当时孟同学也是实打实去做了很多调研,包括从初期的人力投入、开发周期,到后续的计算成本,这些都是需要深入权衡ROI的因素。
当我问到,如何平衡收益和建设时,孟同学说一般从以下角度进行权衡:
一是聚焦核心应用场景:我们在选择大模型应用场景时,会优先聚焦那些能够带来最大业务增值的领域。例如,在客户服务和社区管理等场景中,大模型可以有效提高自动化水平,改善用户体验,从而大大提升效率,预期收益也比较显著。通过与算法团队的紧密合作,将大模型的应用精准落地到这些高价值场景中,我们能够在资源有限的情况下,最大化模型的价值和投入产出比。
二是持续优化大模型性能,降低大模型部署成本:大模型的部署成本高是不可忽视的现实,但我们注重的是在实施过程中持续优化大模型的性能,并结合社区最新的大模型推理优化技术进行调整。例如,我们引入了最新的Radix Attention,并行推理,大模型量化,DeepSeek MTP推理加速等技术,结合得物的具体业务场景,进行多方面的性能优化。这些优化不仅提升了大模型在实际应用中的效果,也有效降低了大模型部署的成本,从而实现更好的ROI。
三是通过资源池合并,多部门共用GPU资源的方式降低大模型训练与推理成本:在这方面,我们通过构建多个部门共用的大模型训练资源池,来降低大模型训练成本。在推理阶段,我们通过复用空闲的GPU资源,提供大模型的公共服务,等多种方式,使得多个部门可以共享这些资源,从而降低了大模型的推理成本。这种资源池的合并和共享方式,使得我们能够更加高效地利用公司现有的计算资源,降低整体的开销。
四是持续优化大模型训练与推理平台的效率,缩短上线时间:在训练与推理平台的效率优化上,我们做了大量的工作,通过构建更加高效的训练和推理流水线,减少了大模型上线的时间。同时,我们通过一键微调、一键部署等功能,使得业务方同学能够快速根据业务需求调整和部署大模型。这个自动化的流程不仅提高了工作效率,也大大缩短了从模型开发到实际应用的周期,进一步提升了项目的ROI。
通过上述方法,他们在建设大模型平台时能够尽量控制成本,优化投入产出比,确保大模型的业务落地在合理的时间周期内能带来可观的收益。
为什么做这个项目?
ROI 和团队说清楚了之后,还要跟团队说清楚,我们为什么要做「大模型训练推理平台」,以及怎么去做?
在这个项目中,孟同学担任了一个二合一的角色,既负责产品设计,也负责功能开发。时间回溯到2022年底至2023年初,伴随着ChatGPT的发布,大模型概念的爆发式增长**。与此同时,公司内部也有很多同学开始关注如何部署大模型、如何利用大模型为他们的业务带来实际的收益。当时面临的问题,业务需求不断增加,大模型不断发展,没有统一大模型专用平台,更别说利用大模型来为业务做有效支撑和带来实际收益了。
孟同学就意识到,必须尽快构建一个大模型的专用的平台,让大家能够在这个平台上以低门槛的方式使用并接入大模型。在这个背景下,孟同学他们就开始构建一站式大模型训练与推理平台。
怎么做?
在落地这个项目过程中,还需要考虑到几个实际问题。
首先这个平台支持大模型的快速部署。伴随大模型概念的火爆,通过Lora**微调大模型的方式,因其成本低,效果好,很快流行出来了。于是他们把大模型微调功能也加到平台上了。这样很多业务方便可以使用少量数据,以较低的成本快速微调他们自己的专用大模型。这个便是一站式大模型训练与推理平台最初的架构。但那个时候很多云厂商都还没有相关的平台,落地全靠一步步摸索。
从收益角度来看,首先,通过集中训练与统一部署大模型,我们可以进行统一的优化与资源配置,显著降低了大模型训练与部署的成本。其次,这个平台打破了技术壁垒,使得公司内部非算法同学也能够通过平台自助式操作,基于自己的数据进行模型微调与快速部署。
最终「一站式大模型训练推理平台」也是在得物内部顺利落地,不仅极大降低了大模型接入成本,在社区、客服、公司内部应用等场景成功落地,增强了业务价值与用户体验。
项目成功上线后,有项目小伙伴吐槽到,孟同学对「用户动线设计/代码注释规范」简直是有“强迫症”。
孟同学表示,强迫症肯定没有,就是有点爱抠细节。
在每个项目开始之前,他都会与业务方进行详细的需求梳理,并通过多轮评审确保需求的准确性和可执行性。这种做法在开发大模型平台时,帮助他们避免了许多潜在的风险。
他还说:“在工作中,细节直接影响整体效果,尤其是在用户动线设计和代码注释规范上。用户动线设计关系到用户体验的流畅性,而代码注释则是团队协作的关键,能帮助成员快速理解和优化代码。任何细节上的不足,都可能导致后续问题的产生。
例如,平台操作步骤过于复杂或逻辑不清晰,会直接影响用户体验和平台的使用频率。在大模型平台的设计过程中,通过反复优化用户操作流程,简化步骤,减少不必要的点击,确保用户体验顺畅。”
四、自驱与成长:补齐短板,让长板更长
从项目牵头设计到最终落地,孟同学的成长无疑是非常快的,在沟通过程中,还发现孟同学是一个「自驱」的小伙伴,入职后,主动牵头了向量数据库Milvus平台构建这个项目。这对他来说完全是一个全新的领域,但是他竟然可以在短时间学习相关知识,快速补齐短板。
他说,“我有幸牵头了Milvus向量数据库平台的建设项目。虽然我之前有一定的数据库和分布式系统的经验,但向量数据库的应用和优化对我来说是一个全新的领域。这个挑战让我希望能够学习并掌握更多技术,拓宽自己的视野,提升专业能力。”
为了尽快弥补不足,他采取了两个方法。
一是,通过多种途径学习Milvus相关的理论和实践,深入理解其原理和应用,特别是如何处理大规模向量数据、优化索引和提升检索效率等。这样,不仅积累了经验,还能帮助他在项目中做出更加合适的技术决策。
二是,加入了Milvus开源社区,积极与社区的开发者和专家进行交流,主动去向社区专家请教问题,了解他们的经验和解决方案。这种互动不仅让他学到了很多实用的知识,还获得了很多帮助,也让他能更好地理解Milvus的最新动态和功能。
在项目的早期,Milvus的某些版本在高并发和大规模数据量下存在稳定性问题。为了解决这些问题,他们进行了多次性能压测,分析系统瓶颈并向社区反馈,最终在社区的帮助下逐步优化了性能,确保了平台的稳定和高效运行。这个过程中,他积累了Milvus的系统调优经验,也加深了对Milvus架构的理解。
除了工作上的成长外,孟同学还经常受到来自外部行业大会的邀请去分享相关的实战经验。孟同学说,“我认为行业分享是自我成长的途径。” 在准备分享时,他会回顾自己的工作,思考技术的有效性,帮助他识别和改进可能忽视的细节。
他认为,“每次分享不仅是与他人交流,也是提升自己知识体系的机会,促使自己可以不断学习和拓展知识。分享还可以结识行业内外的优秀实践和新朋友,吸收新见解,拓宽视野,促进与其他专家和企业的合作,这对个人成长和公司影响力都很有益。”
对他来说,分享虽然需要消耗时间和精力,但他认为这是长期投资,提升个人影响力,推动团队进步,为公司带来更多价值,这是一件值得长期去做的事情,不断地去通过持续学习、分享,自己也会不断的向前探索。
五、工作与平衡:“计划驱动”和“灵活调整”
有时候他们也会面临紧急项目,当项目和生活中重要事情冲突时,孟同学表示,“我始终保持 “计划驱动”和“灵活调整” 相结合的方法,以确保项目按时交付并达成预期的业务结果。我的经验是先从小规模验证开始,再逐步扩大应用,确保每一步都有清晰的反馈和调整。”
以大模型平台项目为例,在项目初期,他们构建了最小可行产品(MVP),并邀请相关同学进行试用。虽然前期看似投入了较多时间,但通过小范围的验证,能够在功能扩展前发现潜在问题,确保后续的推广和扩展更具保障。这种方式避免了大规模投入后发现问题的风险,并让他们能在优化过程中积累实际经验。
类似地,在进行推理服务性能优化的CPU与GPU分离项目时,也是先进行了小范围验证,并在验证效果良好后,才将其正式上线并在更多业务中推广。尽管前期验证看起来会浪费一些时间,但通过实际数据的反馈,他们就及时优化了方案,最终大规模部署时效果显著,节省了成本并提升了性能。
他表示,“这种逐步推进、快速反馈与调整的方法,帮助我们在高压环境下保持灵活性,确保项目能在预定时间内顺利完成,并且保证了最终的业务收益。”
六、展望与建议:保持成长型思维,勇于突破自我边界
孟同学说“未来三年,他也会持续专注于大模型的部署性能优化和应用场景落地。”
当前,大模型的推理性能和高昂成本是制约其广泛应用的主要因素,特别是在计算资源和效率方面。与此同时,像Rag,Agent这样的应用场景在各行业的落地也面临一些技术挑战,仍需要更深入的研究。
他也有自己一些学习方向分享给大家。
一是通过多种方式为自己积累相关的知识和经验。比如参与一些项目,关于如何优化大模型的计算效率,并降低推理成本。
二是积极参与开源社区的讨论,跟踪相关领域的技术进展。通过阅读最新的论文和开源代码,去了解了当前大模型优化的前沿技术,并从一些专家那里获得了宝贵的指导,帮助他更好地理解这一领域中的技术挑战。
他也会持续关注新的场景融合,比如,探讨如何将大模型与云计算结合,特别是在云原生环境下如何提高资源调度效率,进一步提升大模型的训练和推理性能。
我们也相信在未来三年,孟同学在大模型的优化和应用落地方面会有更多的积累,并能为行业提供更加实用的技术解决方案。一说到孟同学都纷纷说,对,就是那个大佬!
当问到他对新入职的小伙伴有什么建议时,他说: “保持成长型思维,勇于突破自我边界。”
在职场初期,很多人会遇到不熟悉的工作和挑战,可能感到不安或迷茫,但这些正是成长的机会。孟同学在入职初期也经历了不少挑战,特别是在跨部门协作方面。刚开始时,他会对如何协调各部门的需求和资源感到不确定。
当时他参与的一个大模型平台项目,项目初期需要与多个部门沟通确认需求,每个部门的系统和流程都不相同,信息的对接和沟通也很复杂。为了快速推荐和落地,他主动向经验丰富的同学请教,逐步了解各团队的工作流程,并通过与各部门的同学逐一沟通,确保每个环节都能顺利衔接。
他说,“不必害怕犯错或显得不成熟,向有经验的同学请教能让我快速融入团队,学到更高效的工作方法。同时,我也学会了通过反思总结,不断找出自己的优点和不足,每完成一个任务后回顾自己在其中的表现,这让我在之后的工作中更加从容、不断提升。职场中的很多机会常常来源于那些需要学习新技能、走出舒适区的挑战。虽然这些任务看似困难,但正是通过解决这些困难,才能带来更大的成长空间。因此,保持开放的学习心态和积极迎接挑战,是职场新人最重要的品质,它不仅能帮助你在工作中不断进步,也为未来的职业发展打下坚实的基础。”
通过和孟同学的对话,我们看到的不仅是一个将“反复打磨”刻入日常的细节控,更是一个在时代快变中锚定自我进化节奏的长期主义者——他用行动验证:真正的成长从非宏大口号,而是把每个需求拆解为精进机会,将每次压力转化为认知升级的燃料,在“自驱”而非“他驱”的节奏中拓宽能力象限。
当组织与个体形成双向奔赴的成长型契约,那些被认真对待的代码、反复推敲的方案、深夜迭代的模型,终将沉淀为个人不可替代的价值坐标。你就只管往山顶走,走过的路自然都会变成我们的台阶。 你要坚信,时间从不辜负认真打磨自己的人。共勉!
往期回顾
1. 最近爆火的MCP究竟有多大魅力?MCP开发初体验|得物技术
2. 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 得物技术
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
最近爆火的MCP究竟有多大魅力?MCP开发初体验|得物技术
一、前言
MCP 全称 Model Context Protocol,是由 Anthropic公司在 2024 年 11 月推出一个开放协议,主要用于标准化应用程序向大语言模型提供上下文的方式。可以将 MCP 想象成 AI 应用程序的 USB-C 接口。就像 USB-C 为设备连接各种外设和配件提供了标准化方式一样,MCP 为 AI 模型连接不同的数据源和工具提供了标准化方式。
近期 MCP 的热度持续上升,网上也是喷涌出大量相关文章,相信在不远的将来 MCP 将成为每个开发者必备的技能之一,非常值得投入时间学习一下。下面会通过简单的实践来带大家理解一下 MCP 的工作原理,以及展望下 MCP 在未来可能的一些应用场景。
二、MCP 基础架构
基础架构
在开始实践之前,还是简单介绍一下 MCP 的基本架构和一些基础组件:
-
MCP Host:需要通过MCP访问数据的程序,例如 Claude Desktop、Cursor**、Cline等桌面工具。
主要职责:接受&返回你的提问、跟模型交互、内置了 MCP Client,与服务器保持一对一连接的协议客户端。
-
MCP Server:轻量级程序,每个程序都通过标准化的模型上下文协议 (MCP) 提供特定功能。
主要职责:能力暴露(操作本地文件&浏览器,访问数据库,访问远程服务)。
-
本地数据源:MCP 服务器可以安全访问的数据库、本地文件、浏览器等。
-
远程服务:MCP 服务器可以通过互联网(例如通过 API)连接到的外部系统。
工作流程
从用户提问,到最终完成任务的完整流程可参考下图:
百闻不如一见,百见不如一练。下面我们手把手开发一个 MCP Server,并且通过 Cline 来使用它,实践过程中会容易帮助我们去理解 MCP。
三、MCP Server 开发&实践
准备MCP Client
这里我用的是 Cline,是 VSCode** 中的一个插件,直接在 VSCode 插件市场中搜索安装即可,其实这里的 Cline 在 MCP 的概念中是 MCP Host,只是 Host 里面内置了 MCP Client(负责跟模型&MCP Server 交互)。
其实更推荐使用 Claude,但是 Claude注册流程相对复杂一点,对网络环境要求也更高(需要科学上网)。
安装好后,第一步就是需要配置大模型,这里我选择的是 DeepSeek。
需要自行购买 API Key(platform.deepseek.com/api_keys)
然后就可以开始配置 MCP server 了,点击右上角的第二个图标。
这里可以使用开源的 MCP Server,也可以使用自己开发的 MCP Server,下面我们尝试自己动手开发一个简单的 MCP Server。
开发MCP Server
想要开发一个 MCP Server,并不需要关心协议本身的一些细节,因为官方推出了各种语言的 SDK (modelcontextprotocol.io/sdk/java/mc… ,通过 SDK 可以快速搭建一个 MCP Server,并且主流语言都针对 MCP 推出了自己的框架,Java 也不例外,这里我们选择使用 Spring 框架来搭建一个 MCP Server (docs.spring.io/spring-ai/r…
引入依赖
<dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-mcp-server-webmvc</artifactId></dependency>
定义 Tools
这里我们定义一个发送飞书消息的工具类:
import org.springframework.ai.tool.annotation.Tool;import org.springframework.ai.tool.annotation.ToolParam;import org.springframework.stereotype.Service;import com.lark.oapi.Client;import com.lark.oapi.core.cache.LocalCache;import com.lark.oapi.core.enums.AppType;import com.lark.oapi.service.im.v1.enums.MsgTypeEnum;import com.lark.oapi.service.im.v1.enums.ReceiveIdTypeEnum;import com.lark.oapi.service.im.v1.model.CreateMessageReq;import com.lark.oapi.service.im.v1.model.CreateMessageReqBody;import com.lark.oapi.service.im.v1.model.CreateMessageResp;import java.util.concurrent.TimeUnit;/** * @author xinyi */@Servicepublic class LarkService { private final Client larkClient = feishuClient(); public Client feishuClient() { return Client.newBuilder(System.getenv("larkAppId"), System.getenv("larkAppSecret")).appType(AppType.SELF_BUILT) // 设置app类型,默认为自建 .tokenCache(LocalCache.getInstance()) // 设置token缓存,默认为内存缓存 .requestTimeout(10, TimeUnit.SECONDS) // 设置httpclient 超时时间,默认永不超时 .logReqAtDebug(false) .build(); } @Tool(description = "用飞书给用户发消息") public String sendLarkCardMessage(@ToolParam(description = "接收人邮箱") String receiveEmail, @ToolParam(description = "飞书卡片内容(参考飞书文档要求的结构体)") String cardContent) throws Exception { CreateMessageReq req = CreateMessageReq.newBuilder().receiveIdType(ReceiveIdTypeEnum.EMAIL.getValue()) .createMessageReqBody(CreateMessageReqBody.newBuilder() .receiveId(receiveEmail) .msgType(MsgTypeEnum.MSG_TYPE_INTERACTIVE.getValue()) .content(cardContent) .build()) .build(); CreateMessageResp resp = larkClient.im().message().create(req); return resp.getMsg(); }}
这里 Spring 会自动把@Tools 注解的方法按照 MCP 标准暴露出来,大模型会根据其中的描述来决策是否可以调用此方法。
启动类
import org.springframework.ai.tool.ToolCallbackProvider;import org.springframework.ai.tool.method.MethodToolCallbackProvider;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.annotation.Bean;@SpringBootApplicationpublic class McpServerApplication { public static void main(String[] args) { SpringApplication.run(McpServerApplication.class, args); } @Bean public ToolCallbackProvider weatherTools(LarkService larkService) { return MethodToolCallbackProvider.builder().toolObjects(larkService).build(); }}
打包
到这里一个简单的 MCP Server 就已经开发完成了,下面只需要执行 mvn clean package 打成可执行 jar 包就能配置到 Cline 中了。
配置MCP Server
回到 VSCode 的 Cline 插件,点击第二个图标,然后点击下面的 Configure MCP Servers,然后开始编辑右侧的配置文件:
这里的配置文件是 MCP 标准化的,下面基于我们这个 MCP Server 介绍下几个核心配置的含义:
"mcpServers": { "lark": { "disabled": false, "timeout": 60, "command": "/Users/admin/Documents/jdk-17.jdk/Contents/Home/bin/java", "args": [ "-Dspring.ai.mcp.server.stdio=true", "-Dspring.main.web-application-type=none", "-Dlogging.pattern.console=", "-jar", "/Users/admin/Documents/git/open-source/spring-ai-mcp-server-demo/target/spring-ai-mcp-server-demo-1.0-SNAPSHOT.jar" ], "env": { "larkAppId": "xxx", "larkAppSecret": "xxx" }, "autoApprove": [ "sendLarkCardMessage" ], "transportType": "stdio" },
-
mcpServers:JSON 配置跟 Key
-
lark:MCP Server 唯一标识&名称
-
command:启动 MCP Server 的命令(如 Java 就是 java -jar,Node 一般是 npx,Python 一般是 uvx)
-
args:执行命令后面的自定义参数
-
env:环境变量,用于配置一些可配置参数,比如密钥、外部 URL 等
这里配置好了后,如果右上角的点变成了绿色说明 MCP Server 加载成功,而且在下面还可以看到 MCP Server 提供的所有 Tools,以及每个 Tool 的参数跟描述。
开始体验
点击右上角的+号开始聊天:给我发一条下午好的飞书卡片消息,附带一下今日的热点新闻。
可以看到 Cline 调用了大模型开始思考,并且根据 MCP Server 提供的 Tools 开始选择发送消息接口并执行。
而且如果第一次尝试失败,还会自动纠错,最后成功调用了我们 MCP Server 提供的 Tools,发送了一条消息给我。
进阶体验
上面的例子我们只用到了一个 Tools,我们可以尝试组合多个 Tools&多个 MCP Server 来实现更复杂的任务,比如我们现在再开发一个可以操作 ES 的 MCP Server,然后打包后配置到 Cline 中。
@Tool(description = """ 通用ES查询工具,参数示例: path: 请求路径 method: HTTP请求方法 GET 或 POST queryJson: 具体请求体 """)public String searchByQuery( String path, String method, String queryJson) { String url = String.format("%s/%s", System.getEnv("esBaseUrl"), path); HttpEntity<String> request = buildEsRequest(queryJson); ResponseEntity<String> response = restTemplate.exchange( url, HttpMethod.valueOf(method), request, String.class); return response.getBody();}
配置好后,在对话中发送:分析一下 es 集群目前的索引分布,重点分析一下哪些索引的分片设置不合理,最终整理后飞书发给我。
然后会根据请求 ES 返回的结果,再次吐给模型进行分析。
最终整理后通过飞书发送一份简单报告。
联想一下
想象一下,如果组合一下飞书文档、浏览器操作、文件系统、发布系统对接等 MCP Server,一句话就可以让大模型从自动连接浏览器,打开飞书文档,分析需求,分析视觉稿,然后自己写代码,对比视觉稿,你就喝杯咖啡,静静的看着它工作。
顺带推荐一下常用的 MCP Client 以及一些现成的 MCP Server。
-
MCP Client List:
-
MCP Server List:
四、总结
相信大家通过上面的实践后,对 MCP 有了一个基本的认识,组合多个 MCP Server 的工作流可以自主完成非常复杂的任务,关键是这协议统一了连接标准,有大量现成的 MCP Server 可以即插即用,大幅降低建设成本。总之 MCP 协议的持续落地,让 AI 不再只是聊天工具,而是工业智能革命的万能操作平台,在未来潜力无限,想象无限,值得每一位开发者去学习并掌握它!
往期回顾
1. 得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
文 / 新一
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
大语言模型的训练后量化算法综述 | 得物技术
一
简介
在模型轻量化领域,量化是一种用于减少神经网络模型大小和计算量的技术,将模型参数(权重)或中间变量(激励)从高精度类型(FP32, FP16, BF16等)转换为低精度类型(int8, int4, fp8等)。 而近年来随着Transformer,MoE等架构的提出和大模型的兴起,使得神经网络模型能轻松突破几十亿甚至上万亿的规模参数,因此,我们需要一些适应于大模型的压缩技术,来降低模型的部署成本,并提升模型的推理效率。
从最初的GPTQ、AWQ等weight-only的量化算法开始,到现在LLM从训练、推理、轻量化、Agent等所有赛道都卷到飞起的时代,基于大模型的特性,在两年多时间里业内已有很多新的量化算法。
二
概念
以下介绍一些模型量化中的概念。
量化
-
量化感知训练(Quantization Aware Training, QAT):训练过程中插入伪量化算子,通过训练时统计输入输出的数据范围并动态调整量化参数,将量化过程结合到模型的训练过程中,适用于对模型精度要求较高的场景。
-
训练后量化(Post Training Quantization, PTQ):模型训练完成后对其参数进行量化,通常只需要少量校验数据或不需要校验数据,简单高效,不需要训练,但通常相比QAT精度损失略大。
由于LLM通常训练成本巨大,所以PTQ在LLM中通常是主要的量化选择,本文后续主要介绍各种PTQ的方案。
量化对象
-
Weight:即模型的权重,在LLM中主要指Linear算子的权重。权重量化可减少模型显存开销。
-
Activation:在模型前向计算过程中的输入输出变量,通常不会单独量化激励张量,而是结合权重量化一起。在LLM中激励矩阵的数值变化范围相比权重更大,有更多离群的异常值,因此相比权重量化更难。
-
KV Cache:除了权重和激活之外,在LLM的 KV Cache作为减少重复计算的特殊存在,会消耗不少的显存。 因此,KV Cache量化在LLM推理中减少显存开销,提升吞吐也很重要。
在LLM中,对Weight和Activation而言,通常有只量化权重的weight-only方法和weight & activation都量化的方法;另外为减少KV Cache的计算开销,也有对其进行量化。
细粒度
-
per-tensor量化:逐张量量化,或逐层量化,每个张量只有一个缩放因子。
-
per-channel 量化:逐通道量化,每个通道都有不同的缩放因子。
-
per-token 量化:主要对transformer中的激励矩阵而言,即逐行量化。在LLM中,常与权重per-channel 量化配合使用。
-
per-group:以组为单位,多个元素成组共享一个缩放因子,如GPTQ、AWQ常用的128个元素为一组进行量化,将通道划分为更小的子组,以实现更细粒度的精度控制。
其他维度
分类维度 | 类型 | 对比特点 | 适用范围 |
---|---|---|---|
按是否需要额外校验数据 | 静态量化 | 不需要,通常速度较快。 | 常用于权重量化 |
动态量化 | 需要额外校验集对模型进行前向推理或后向传播,根据推理结果动态计算量化参数;相比静态量化速度较慢。 | 适用于权重量化和激励量化 | |
按量化过程的时机 | 离线量化 | 在模型上线推理前,提前计算量化参数。 | 常用于权重量化和激励量化 |
在线量化 | 在推理过程中实时计算量化参数。 | 常用于LLM中的激励量化 | |
按量化步长是否均匀 | 线性量化 | 量化步长固定,表示的范围均匀。计算复杂度低,硬件友好。 | 常用于基于通用GPU的量化方案 |
非线性量化 | 量化步长不固定,表示范围更灵活。精度损失更小,但计算复杂度高。对硬件支持要求更高。 | 用于基于专用芯片的量化方案 | |
按量化范围是否对称 | 对称量化 | 量化数据范围以零值对称。零点值(zero-point)固定为0值,仅需考虑缩放(scale)参数。 | 用于权重量化和激励量化 |
非对称量化 | 量化数据范围为非对称。zero-point和scale参数都要计算。 | 权重量化和激励量化通常不会同时为非对称量化 |
三
量化方法摘要
GPTQ
GPTQ是一种weight-only的量化方法。它的特点是通过Hessian矩阵对每层权重做逐列量化,并在每列量化中通过反馈补偿之前的量化损失。它是LLM早期主要量化算法,因量化速度快和量化损失小,是早期在实践中被应用最广的算法。具体细节可参见之前的文章:
模型量化与量化在LLM中的应用。
GPTQ算法流程(图片来源:参考文献[1])
AWQ
AWQ(Activation-aware Weight Quantization) 也是一种weight-only的量化算法,也是早期主流的LLM量化算法,其特点是量化速度相较于GPTQ更快,且量化损失在多数量化方案和模型上相较于GPTQ也更小,到目前为止也是一种非常实用的量化方案。
AWQ出自深耕深度学习轻量化多年的HanSong团队,其主要原理是根据前向推理中的对应激励矩阵各个通道的数值,而非权重矩阵的通道数值来衡量权重矩阵各个通道的重要性,从而自动检索每个通道的缩放因子,并进而在优化损失函数中减小量化误差。具体细节也可参见之前的文章:模型量化与量化在LLM中的应用。
AWQ中的平滑过程(图片来源:参考文献[3])
HQQ
HQQ(Half-Quadratic Quantization)也是一种weight-only的量化方法,由其名称可知通过半二次优化的方法得到量化参数。相比AWQ和GTPQ,HQQ不依赖于校验数据集,不从最小化输出激励的角度优化,而是直接从权重本身优化量化前后的权重误差;而且其量化速度特别快,并且在低精度量化上有较好的量化误差。
优化目标如下,最小化原权重与量化反量化后的权重之间的误差,为
范数。
-
为量化参数(zero point和scale)
-
为量化、反量化过程。
-
损失函数为
范数,P<1, 相比于
范数的均方差,
范数更关注权重数值中的长尾奇异值(outliers),以及矩阵的稀疏性,然而其非凸(non-convex)的特性需要优化函数做一定的转化。
优化过程
基于上述问题,引入一个额外变量让主优化函数分割成2个子优化问题;同时,为了方便使用迭代更新的过程解题,我们固定尺度参数
,从而只优化零值
通过交替优化的方法,可以写出如下子问题,以及超参的更新,
sp1的形式是近端算子,对于范数,存在一个广义的阈值解如下,
sp2可以通过量化公式代入,得到如下,
并通过进一步简化(基于W的quantization grouping维度取均值),
表现性能
HQQ的量化耗时非常短,以Llama2-70B为例,在A100上相比于GPTQ和AWQ,耗时分别缩短为1/50和1/25,同时也有着不逊色于前两者的量化精度损失;而Llama2-7B模型的量化耗时更是缩短到1分钟左右。
Llama2-7B量化:GPTQ, AWQ, HQQ三者的耗时对比
(图片来源:参考文献[6])
Llama2-70B量化:GPTQ, AWQ, HQQ三者的耗时对比(图片来源:参考文献[6])
HQQ 在group-wise量化模式下与GPTQ, AWQ等的性能对比(图片来源:参考文献[6])
SmoothQuant
SmoothQuant 是LLM量化领域首个对weight和activation做全量化,并能保障良好的量化损失,从而在实际中有广泛应用的量化算法,并以被Nvidia和Intel集成到各自的大模型量化工具TensorRT-LLM和Neural-Compressor中。SmoothQuant 也是由HanSong团队提出,因此也可在算法中看到相似的通道缩放操作。
该方法直接聚焦LLM量化困难的最主要原因,即transformer推理过程中的激活值中的异常值(Outliers)。激励矩阵中的异常值指的是绝对值比大多数的值大得多的元素值,异常值一直是量化领域的难点,是量化损失的重要来源,而LLM中的异常值尤难处理,因其通常持续存在于部分通道中,且量化过程中对其直接截断处理会对模型的生成能力造成重大影响。
SmoothQuant中量化难度的迁移:激励矩阵中异常值的平滑(图片来源:参考文献[4])
该方法的核心是通过逐通道的缩放变换,使得Activation矩阵的绝对值幅度变得平滑,从而变得容易量化,而为了保障计算一致性,将反缩放因子作用到Weight中,稍微增加了Weight的量化难度;从而整体上使得模型的量化难度降低,且提高了量化精度。
量化过程
Transformer中常规的矩阵乘法表示为,SmoothQuant的矩阵乘法则表示如下,
激励矩阵在列维度上每个元素除以
, 权重矩阵
在行维度上每个元素乘以
,从而完成了对激励矩阵的平滑,以及保持整个乘法计算的一致性。
通道维度的缩放因子用对角矩阵表示,而如何对
取值呢?作者提出了几种方案,
-
一种是利用激励矩阵各个通道的绝对极值,即
-
一种是利用权重矩阵各个通道的绝对极值,即
本质上,缩放因子的大小取值表达了我们要将激励矩阵量化难度的多少转移给权重矩阵。而以上的前者,容易将激活的量化难度向权重过度转移,从而导致权重量化难度大大增加;而后者会直接导致权重各通道的极值都相同,而激励依旧很难量化。
因而,一种平衡的方式如下,用表示迁移强度,来控制激励量化难度迁移到权重的强度,
而当时,下图表示了乘法计算中的缩放平滑过程。X 和W 在各自对应的通道计算绝对极大值,随后通过
这两个向量计算得到缩放矩阵,再对X 和W 两个矩阵进行缩放变换,最后再对两个变换后的矩阵做乘法。
SmoothQuant矩阵乘法中的平滑过程示例(图片来源:参考文献[4])
表现性能
在量化模型的效果上,对比了同为Weight-activation量化的几种算法,SmoothQuant在多个数据集上的准确率表现突出。但作者没有对比同时代下的GPTQ、AWQ等weight-only的效果。
SmoothQuant与W8A8和LLM.int8()的量化效果比较(图片来源:参考文献[4])
而在吞吐上,作者用CUTLASS实现了INT8乘法kernel,并将SmoothQuant集成到Pytorch之后,以W8A8方案为例,实现了在OPT模型上相比于原FP16模型在速度上1.56倍,以及在显存上1.96倍的优势。
SmoothQuant经算子优化后与FP16和LLM.int8()的推理吞吐性能比较(图片来源:参考文献[4])
QuIP
在基于正交矩阵旋转优化大模型量化中的异常值(Outlier)问题的思路中,QuIP(Quantization with Incoherence Processing)是较早提出的一个方案。这种思路与SmoothQuant一样,都是在真正的量化步骤之前,通过对权重矩阵或激励矩阵做一定的前处理,使得该矩阵中的异常值改善或消失,使矩阵平滑,同时在整个前向推导中还能保持计算一致性。而与SmoothQuant直接对目标矩阵做尺度缩放不同,这种思路通常是通过对目标矩阵左乘、右乘正交矩阵,使得矩阵变得更容易量化。
该方案的主要主要亮点如下,
-
一是分析矩阵中元素的绝对值分布,并定义了不相干性,将一个矩阵的量化难易程度具象化。
-
二是提出了基于正交矩阵LDL分解的对权重矩阵的逐列量化方案,并证明了GPTQ也是该算法下的一种特殊情况。
-
最后在低比特量化情况下,该算法证明了其性能优于之前的方案。
矩阵的不相干性
作者定义了基于值的不相干性:当一个Hessian矩阵
可以通过特征分解得到
,且对所有的
满足如下,
那么我们说是的。
而对于权重矩阵, 则定义其 如下,
以上定义中,矩阵的最大绝对值受限于值,而
值越小,则越不相干,对应地,矩阵中的异常值就越少,也越容易量化。
量化过程
整体算法过程涉及到较为复杂的数学推导过程和大量定义和论证,其主要过程如下,
-
第一步,对权重矩阵
做不相干性的前处理,使
更容易量化,并作简单的量化处理;
-
第二步,对Hessian矩阵(用
计算,与GPTQ相同)做LDL分解;
-
第三步,对
进行逐列量化,每次量化当前列时,考虑前面所有已量化列的误差为反馈以缩小量化误差;
-
第四步,逆不相干处理,以及反量化。
QuIP算法的量化过程(图片来源:参考文献[5])
LDLQ
作者定义了一个基于LDL分解的最优化近似算法,自适应的近似过程可以是近似或随机(Near or Stochastic)。 根据以下公式逐层优化,
作者定义了一个基于LDL分解的最优化近似算法,自适应的近似过程可以是近似或随机(Near or Stochastic)。 根据以下公式逐层优化,表示浮点权重,
表示量化后的权重,
表示输入矩阵,
是其二阶矩阵,Hessian矩阵。
而对于每层Linear的,用如下形式作逐列量化,
表示第k列权重,而
表示第1到k-1列,
表示量化后的第k列权重,Q表示对应的Near或Stochastic近似方法选择。
表示某种序列的向量,也正是需要通过LDL分解求的校正项的系数。而整体的量化过程可以用矩阵的形式表示,用一个上三角矩阵表示LDL分解的系数矩阵,即
组成的矩阵,
具体的不相干性处理和逆处理的算法过程可以参考论文中给出的细节。
QuIP中不相干性的前处理和逆处理过程(图片来源:参考文献[5])
表现性能
下图是作者给出的对OPT模型权重层的处理前后,各层的元素值的不相干性的变化,可见在处理后,最大绝对值下降十分明显。
OPT-2.7B模型在不相干性处理前后异常值数量的变化(图片来源:参考文献[5])
而在量化效果上,对比了同为weight-only的主流算法OPTQ(即GPTQ)在同比特情况下,多个验证集的准确率。在对Llama2-70b模型的低比特量化中,尤其是2-bit和3-bit, QuIP的效果明显,且没有崩坏。
QuIP与OPTQ(GPTQ)在不同比特下的量化效果比较(图片来源:参考文献[5])
QuaRot
QuaRot(Quantization scheme based on Rotation)是基于旋转矩阵变换的一种量化方案,它的量化对象包括weight,activation以及KV cache。通过旋转矩阵,在保持一致性的前提下,去除中间变量的异常值,从而使量化更容易,这种模式应用于transformer中的Attention,KV cache和FFN中的激活值。
旋转矩阵
旋转变换利用的是正交矩阵先简单介绍一些相关的矩阵知识。
-
正交矩阵
是满足
的方阵。
-
旋转矩阵是正交矩阵。
-
Hadamard 矩阵是一个元素值都为{+1, -1}的正交矩阵。
-
Walsh-Hadamard矩阵是维度为
的Hadamard矩阵,
令,
是一个包含从{+1, -1}随机抽取的向量,可知
也是正交矩阵。
计算不变性
令是一个权重矩阵,出现在Attention或FFN Block的左侧 (FFN中的
,
,及Attention中的
,那么可以将左侧乘以正交矩阵
,并通过将输出矩阵(FFN中的
, 及Attention中的
)乘以
来消除这种影响。
上述的计算不变性在当两个Block之间有RMSNorm时也是成立的。因为从概念上讲, RMSNorm对输入矩阵的每一行做归一化(其尺度缩放的参数会被吸收到就近的Linear权重),正交矩阵应用于 activation 矩阵不会影响范数。
那么总的来说,对于一个Activation矩阵,右乘
,使得线性层的输入由
变为了
,被归一化之后送入下一个 Block,该Block 的输入权重现在是
;即原本的
,变成了
, 输出不变,保持一致。
量化过程
QuaRot总体分为2个阶段
- 第1阶段,对transformer的前向过程进行旋转变换,具体是在Attention和FFN过程中插入离线Hadamard变换**和额外的在线Hadamard变换。
- 第2阶段,利用现有的量化方法对weight进行量化,以及将量化过程加入前向过程使得对activation和cache进行在线量化。
第一阶段
第1阶段是对各个环节做Hadamard变换。
原Attention(包括RMSNorm): 实线箭头表示训练期间的变量流向,包括每个token的填充和推理
原FFN(包括RMSNorm):门控前馈网络
QuaRot版Attention:RMSNorm缩放alpha被吸收到输入矩阵,隐藏状态插入在线Hadamard变换进行旋转
QuaRot版FFN:RMSNorm缩放alpha被吸收到输入矩阵,降采样Linear前插入在线Hadamard变换进行旋转。
QuaRot 量化前对transformer各个模块的旋转变换(图片来源:参考文献[7]
阶段1a: 权重调整,遵循计算不变性原理对权重做正交变换
对权重矩阵,例如, 首先,前面的LayerNorm 或 RMSNorm 的线性部分将被融合进来,再左乘随机Hadamard矩阵**
,表示如下,
其中表示归一化op被吸纳的线性部分,而对应输入的激励居住,则变为了
该操作对应Attention中的和FFN中的
,而这样处理后对比处理前,激励不再包含异常值。
旋转变换前后激励矩阵中异常值数量的变化(图片来源:参考文献[7])
阶段1b: 对FFN的输出插入在线Hadamard变换
该操作是针对下采样乘法的输入激励的异常值的处理。由上图可知插入了一个在线Hadamard变换算子,同时对下采样矩阵的参数做了补偿,使得
。
同时为了保障下一个Block的激励输入是带变换的,所以还需右乘一个,使得最终的变换形式是
,保障FFN的输出为
,作为下一模块的输入。
阶段1c: 对Attention模块的注意力和Value的Hadamard变换
作者对注意力块同时应用了在线Hadamard变换和融入权重的离线Hadamard变换。 在计算注意力时,写成每个Head计算的维度,有如下形式,表示相应的Linear权重,
其中,
-
为softmax的输出,是一个维度为序列长度的方阵
-
是单个Head的value矩阵,
-
相乘后与
相乘,上式表示逐Head的Attention模块输出
的计算过程
首先对分别右乘和左乘,做Hadamard变换,带入上式,可知保持计算不变性。
而分别有每个Head维度的
concat而成,可以用单个Kronecker**乘法的形式表示对
的变换,
然后利用如下特性构建完整的Hadamard变换,
-
对
,先右乘了
之后,再进行一次Hadamard Head操作(即
,
表示注意力计算的输出),即相当于又右乘了
,即总体右乘了
。
-
对
,先左乘了
,再左乘了
,所以总体左乘了
。
综上,所以总体上整个过程的设计保持了计算不变性。
阶段1d: 对key的Hadamard变换
Key向量的计算也会收到异常值的影响,所以也需要引入Hadamard变换来消除这个问题。注意力矩阵 计算如下,
其中,,是 输入Softmax时的缩放尺度,
表示mask, 如最常用的Causal Mask,Pos 表示位置编码。
由于Pos的存在妨碍了直接将Hadamard矩阵融合到中,因此也使用了在线Hadamard Head操作来旋转
,对他们右乘
,
其中的相当于变成了
,整个计算过程保持了计算不变性。
第2阶段
第2阶段是在变换后的真正量化过程。
阶段2a: 权重的量化
采用现成的GPTQ,或者更直接、更快速的RTN。
阶段2b: 激励的量化
对输入input进行per-token维度的在线量化,而其中RMSNorm依旧保持FP32的精度。
阶段2c: 缓存的量化
对kv cache直接量化到低比特并存储,并在需要计算时提取并重新反量化到FP16精度,计算乘法。而过程中Query保持FP16, 并参考了类似Flash Attention中的在线Softmax计算方式。
所以,结合上述细节和上图,我们可以讨论整个过程的数据流转。
在Attention过程中,FP16的输入右乘变换后,经过RMSNorm归一化,量化到INT4形式,并与左乘变换并量化后的权重做INT乘法运算,随后再反量化回FP16,其中
经过位置编码(RoPE)计算,而
经过变换并量化保存为cache,且在做MHA时反量化并变换回来,最后到输出Linear时再经变换和量化,与已变换并量化的权重相乘,最终再反量化为FP16输出
。
在FFN过程中,FP16的输入右乘变换后,经过RMSNorm归一化,量化到INT4形式,分别与左乘变换并量化后的上采样权重和门控权重做INT乘法运算,并反量化回FP16,做点乘;最后经变换和量化到INT4,与变换并量化后的下采样权重做乘法,最终再反量化为FP16输出
。
表现性能
在对权重、激励和缓存的全4-bits量化效果对比中,QuaRot相对于SmoothQuant, QmniQuant和QuIK,在Llama模型上有性能优势;且应用了group-wise后,对比Atom也有性能优势。
QuaRot与其他量化算法的性能比较(图片来源:参考文献[7])
SpinQuant
SpinQuant也是一种在利用正交旋转矩阵减少异常值的思路上的量化方法。该量化方案也是一个全量化方案,其量化对象也是所有的权重,激励和KV缓存。
该方案中,作者分析了不同随机矩阵变换下,多次量化效果的稳定性。用普通随机矩阵做旋转变换的量化过程的量化效果,最好与最差之间相比差距多大13个点,而随机 Hadamard 矩阵优于随机旋转矩阵,但也仍有6个点的不可忽略的方差。而作者提出的Cayley优化矩阵,如下图对比,则能将最终量化性能的方差明显缩小。
Llama2-7B 在不同随机旋转矩阵量化到W4A4模型的性能分布。不同随机旋转矩阵(普通随机,Hadamard和Cayley优化矩阵)之间的方差(图片来源:参考文献[8])
插入旋转矩阵
作者提出了针对不同复杂度而定制两种旋转策略。
下图是在完整的transformer block中插入不同旋转矩阵的概图,有四类旋转矩阵:,根据是否能合并,分为两类,
-
2个可合并的旋转矩阵:产生旋转不变的全精度网络。
-
2个在线的Hadamard旋转矩阵:进一步减少极端activation, kv-cache量化的异常值。
由此,作者提出了两种量化方案
-
SpinQuant(NoHad): 仅使用了离线旋转矩阵
-
SpinQuant(Had): 使用了
SpinQuant整体的旋转变换(图片来源:参考文献[8])
R1R2
SpinQuant旋转矩阵的插入和应用与QuaRot大同小异。
由上图(a)(b)可知,和QuaRot中的1a一样,作用于每个Attention和FFN的输入处的激励矩阵,即Attention的Q、K、V Linear输入和FFN的上采样、门控Linear输入;具体到模块内部,其补偿矩阵
$会被吸收到各种的权重矩阵中。
则是Head-wise地将注意力机制的输出乘以
, 随后在输出output的投影矩阵
乘以
。这一操作类比于QuaRot中的1b,其旋转的计算一致性如下,
R3R4
类似于QuaRot中的1c,在注意力机制中插入了额外的在线Hadamard变换(),以及在FFN的降采样Linear之前插入了在线Hadamard变换(
),其旋转的计算一致性如下:
注意力机制中value矩阵的旋转变换(图片来源:参考文献[8])
FFN中下采样输入的变换(图片来源:参考文献[8])
Cayley优化旋转矩阵
本方案的一个主要贡献,是基于上述随机矩阵的方差分析,对旋转矩阵进一步做了基于最小化量化网络误差的优化。优化目标是上述的可合并的 ,而在线旋转
依旧使用了Hadamard随机矩阵,这也是两种方案命名为NoHad和Had的原因。
基于优化过程的损失函数如下,
这里,
-
表示 Stiefel 流形,是正交矩阵的集合,{
}。
-
是基于校准集的比较量化前后的任务损失,可以是交叉熵,是一个关于{
}的函数。
和
分别是权重矩阵和输入激励矩阵。
为了优化上述函数,作者采用了一种叫Cayley SGD的梯度方法,这是一种Stiefel流形上的高效算法。其本质是一个迭代更新的优化过程, 在每次迭代中,旋转矩阵基于梯度更新,
其中定义 ,是对矩阵
的Cayley变换,
是斜对称矩阵(
)。 而
由上述的损失函数的梯度
的一个投影计算得到,
通过矩阵的正交属性,推出Cayley变化得到的矩阵
的正交属性,从而保证更新后的旋转矩阵
的正交属性。通过上述的梯度计算的迭代过程,可以求解优化
,在这个过程中transformer的权重参数保持不变。
在具体实践中,作者基于WikiText2数据集作为校验集,用其中800个样本作前向推导,使用迭代更新次数为100次的Cayley SGD梯度优化结果作为新的旋转矩阵{},并在上述的随机矩阵量化结果分析中取得了最小方差和最优效果。
表现性能
在量化性能上,基于Llama2系列与SmoothQuant、OmniQuant等方案作了比较,也与weight-only的算法GPTQ, AWQ, QuIP等做了比较,有更低的PPL(困惑度)和更好的准确度。
量化效果对比(验证集:基于8个0-shot推理任务的平均准确度和基于WikiText2的PPL; 测试模型:Llama2)(图片来源:参考文献[8])
而且作者也对比了基于优化Cayley旋转矩阵和随机Hadamard矩阵的相同量化方案下的量化效果,体现了控制变量下的优化效果。
Llama3.2, Llama-3, Mistral等在8个0-shot任务下,Hadamard与Cayley优化矩阵的效果对比(图片来源:参考文献[8])
QQQ
QQQ(Quality Quattuor-Bit Quantization)是来自meituan的一个缝合了多种量化手段的方案。它吸收了自适应smooth技巧和Hessian-based权重量化算法,并重写了整型乘法的高效算子库,是一个针对weight和activation全量化的two-stage的量化算法。
QQQ算法的二阶段量化流程(图片来源:参考文献[9])
量化过程
自适应平滑
通过通道维度的缩放操作,使得激励矩阵的异常值变得平滑,从而降低量化难度,这是启发自smoothquant算法,为求最优的平滑系数,构建了如下最小化量化前后输出误差的优化函数,
为element-wise的除法和乘法。
权重量化
基于Hessian的逐列的权重量化算法,则是借鉴自GPTQ,
表示
第
行,
表示Hessian矩阵。
W4A8
为支持和加速不同比特位的整型乘法,重写了W4A8的GEMM算子,融合了整型转换和反量化的过程,如下图,包含了per-channel和per-group 两种方案。
-
Per-channel: INT4的权重
先通过精度转换变成INT8格式,再与INT8的激励
做GEMM, 最后反量化为FP16精度。
-
Per-group: INT4的权重
首先通过精度快速转换为FP16, 再加载group量化参数将权重反量化,随后再精度转换为INT8, 与INT8的激励做GEMM, 最后再反量化为FP16精度。
W4A8的 per-channel权重量化模式(图片来源:参考文献[9])
W4A8的 per-group权重量化模式(图片来源:参考文献[9])
表现性能
在量化效果上,以Llama2为例,基于Wikitext2的PPL和多个测试集的0-shot准确率和同等量化QoQ效果相当,而在PPL上与weight-only算法相比似乎稍有不足。
QQQ与其他算法的量化效果PPL对比(图片来源:参考文献[9])
QoQ与其他算法的量化效果Zero-shot准确率对比(图片来源:参考文献[9])
而在推理性能上,通过基于高性能算子库Marlin重写了GEMM并集成到推理引擎vLLM上,
在Llama2-13B上相比于FP16,SmoothQuant和AWQ,有着2.24×, 2.10×, 1.59×的速度优势。
QQQ量化后的模型与其他算法的推理吞吐性能比较(图片来源:参考文献[9])
QoQ
QoQ(Quattuor-Oct ̄o-Quattuor)是来自HanSong团队的W4A8KV4的低精度全量化方案。QoQ算法及其相关的Qserve推理系统,集成了包括量化过程和算子优化,与其说是量化算法,不如说是一套完整的端到端的模型轻量化推理引擎。
作者在量化比特的选择上对比了Weight-only的W4A16方案和per-channel weight量化和per-token激励在线量化结合的W8A8方案。
-
对于批处理较小的情况,LLM的GEMM主要是内存受限,W4A16有着更高的吞吐。
-
当批次变大时,GEMM就变成了计算受限,由于INT8 Tensor Core具有更高吞吐量而使W8A8显得更快。
-
而作者认为W4A8能兼顾两者,在内存密度和计算密度的场景下都能保持较高的吞吐。
-
而在解码阶段,由于token的逐个生成特性,Attention的计算密度相对较低,因而KVcache的量化有助于解决内存密度问题,对KVCache选择W4,相比与W8能获得更高的性能。
-
而对于更激进的W4A4,一方面由于准确性下降,另一方面也由于W4A4 GEMM在当前GPU架构(Ampere, Hopper)上并没有太显著的提升。
量化过程
QoQ的量化过程是众多量化算法中的技巧融合,通过不同手段来减小量化误差。
渐进分组量化
QoQ的渐进分组量化 (图片来源:参考文献[10])
渐进分组量化(Progressive Group Quantization)指的是对weight 先进行per-channel的INT8量化,再进行per-group的INT4量化。
给定权重,先用per-channel对称量化至INT8形式,
表示量化后得到的INT8的权重,是所使用的量化参数scale。
然后,对上述量化结果再使用per-grouup非对称量化至INT4形式,
表示最终的无符号4-bit量化权重,
分别是对应的量化参数zero-point和scale。
当推理的前向过程计算W4A8 GEMM时,INT4的权重 被加载后,先反量化为INT8的权重
,再执行W8A8的矩阵乘法。
另外,实际中,为了保护INT8反量化时饱和溢出,将INT8对称量化范围从[-127, 127]缩小到[-119, 119]。
平滑注意力
平滑注意力(SmoothAttention)借鉴了SmoothQuant中依靠通道缩放转移激励量化难度的思路,主要针对Key矩阵异常值较多且难量化的问题。
下图可视化了Value矩阵和经过RoPE的Key矩阵的元素值分布,可见Value矩阵的值较为平滑,而Key矩阵中有明显通道固定的异常值。
RoPE输出Key矩阵经smooth前后的异常值变化,以及Value矩阵的异常值变化(图片来源:参考文献[10])
借鉴SmoothQuant, 通过per-channel缩放因子缓解Key矩阵中的异常值范围,
可以通过激励矩阵简单计算,
, 而缩放强度超参
可以取经验值0.5。由上图可见,通过平滑后Key矩阵的异常值得到明显缓解。
而实际中,通常缩放矩阵的补偿矩阵会融合到前一层的权重中去,而LLM中Attention的Query和Key通常会经过RoPE处理。RoPE在每个Head中将通道
与通道
配对。因此为了使SmoothQuant在RoPE中可交换,作者附加了一个硬约束条件,令
,即
Qserve中的平滑缩放优化(图片来源:参考文献[10])
这样则可以通过 和
将缩放矩阵的补偿矩阵融合到Query和Key的Linear层权重中去了。
旋转矩阵
同样,借鉴自QuaRot,QuIP等,使用Hadamard矩阵做旋转变换,来抑制输入激励矩阵的异常值。
Qserve中的旋转矩阵优化(图片来源:参考文献[10])
通道重排
另外,参考AWQ, GPTQ等,提出了基于激励的通道重排序,使用激励矩阵逐通道的最大||值,来表征通道显著性,重新排序通道,使得具有相似显著性的通道在同一个量化组中。
Qserve中的通道重排优化(图片来源:参考文献[10])
Qserve吞吐优化
在通过各种量化技巧融合实现了W4A8KV4的量化流程后,为了保障其推理吞吐性能,作者又设计了一个Serving系统,命名为Qserve,将量化过程融合,设计GEMM kernel, 相当于一个高效的推理引擎。
下图是Qserve runtime示意图,其中所有的GEMM层都使用了W4A8输入并在INT8的TensorCore上执行,输出FP16格式,所有的Attention和LayerNorm都以FP16计算,且整体的LLM模块的输入输出都是FP16格式。
Qserve runtime推理流程中的精度变化(图片来源:参考文献[10])
-
算子融合
对于QKV投影和FFN第一个Linear,激励量化被融合到前面的 LayerNorm 中;FFN层第二个Linear的激励量化,则融合到前面的激活 Kernel 中。
-
KV-cache管理
参考了vLLM、TensorRT-LLM等的PagedAttention模式,相比这些搜索引擎,Qserve采用了逐Head的动态管理模式,因为其需要存放量化参数,以及动态更新。
-
W4A8 GEMM
GEMM是计算的主要开销,Qserve通过对权重重排、Per-channel反量化、Per-Group反量化等做了深度优化。
-
KV4** 缓存
结合KV的量化和反量化优化整体的Attention流程耗时。
表现性能
在量化模型的PPL指标上,基于Llama,Mistral等模型,作者对比了很多量化算法,在同等量化条件下,QoQ有一定的优势,和QuaRot相当,而相比于Weight-only算法稍有不如;而在0-shot的准确率上优于Atom和QuaRot算法。
QoQ与其他算法的量化效果PPL对比(图片来源:参考文献[10])
QoQ与其他算法的量化效果Zero-shot准确率对比(图片来源:参考文献[10])
在推理吞吐上,得益于其对Pipeline的深度优化,Qserve甚至表现得优于TRT-LLM这样的专业推理引擎。
Qserve量化后的模型与其他算法的推理吞吐性能比较(图片来源:参考文献[10])
FP8
FP8是以8-bit位表示的一种低精度浮点格式,Nvidia 的GPU从Hopper架构的显卡开始支持FP8的训练和推理格式。FP8有2种格式,以下是与FP16和BF16的数据格式对比,
FP8两种格式与FP16及BF16的比特位表示对比(图片来源:参考文献[13])
-
E4M3:包含1个符号位,4个指数位(exponent) 和 3个尾数位(mantissa),可以表示[-448, 448] 范围的数值和nan.
-
E5M2:包含1个符号位,5个指数位(exponent) 和 2个尾数位(mantissa). 可以表示[-57344, 57344],正负无穷(inf)和 nan.
FP8两种格式的具体表达范围(图片来源:参考文献[11])
符号位占一位,表示正负。
指数部分在浮点表示法当中,一般会减去一个偏移量,对于FP8 E4M3 而言,这个偏移量为-7,这使得指数的表示范围为[-7, 8]。对于 FP8 E5M2 而言,指数偏移量为 -15,指数表示范围为[-15, 16]。
底数从高位到低位,分别表示2的负k次幂;对于E4M3格式,使用3个比特表示底数,其分别对应2的负1, 2, 3次幂。对于E5M2格式,使用2个比特表示底数,分别对应2的负1, 2幂。底数表示时会额外加1,而当指数部分全为0时,则不额外加1。
浮点量化
相比于整型量化,浮点的量化属于非均匀量化,即浮点量化的步长是不固定的,由下图可知,相比于FP32到INT8的映射,浮点量化的步长随着指数部分的变大而变大。
FP8与INT8量化的量化步长对比(图片来源:参考文献[12])
量化精度
FP8与INT8量化孰优孰劣,只能说各有长短。FP8 相比INT8,有更大的表示范围,但在一定范围内,其精度表达能力相较INT8为差。如下图,从高斯分布随机抽样1000万个数字,分别使用 FP8 E4M3, FP8 E5M2, INT8 完成量化。在三者的量化中,应用不同的缩放参数来调整量化效果,画出量化误差(用MSE表示)。可以看到,FP8之间,E4M3的量化效果要好于E5M2, 而在选取合适的量化参数范围内,INT8的量化效果要好于FP8,而FP8具有更好的兼容性,对缩放参数的选择相对不敏感,更适合不需要校验集的量化。
FP8与INT8在不同量化参数下,对正态分布数据量化的精度损失对比(图片来源:参考文献[12])
四
总结
下面是对以上介绍的一些大模型量化方案的简要总结和对比,
量化算法名称 | 量化对象 | 特点和适用范围 |
---|---|---|
GTPQ | 权重 | 离线量化,支持4~8-bit,量化速度较快,支持模型较多,比较成熟 |
AWQ | 权重 | 离线量化,支持4~8-bit,量化速度较快,支持模型较多,比较成熟 |
HQQ | 权重 | 离线量化,支持1~8-bit,量化速度在所有算法中最快,量化精度与GTPQ,AWQ相当 |
SmoothQuant | 权重、激励 | 在线量化,支持8-bit,量化速度较快,支持模型较多,比较成熟,推理吞吐较快 |
QuIP | 权重 | 离线量化,支持2~4bit,量化速度较快,低精度(2-bit)下效果优于GPTQ |
QuaRot | 权重、激励、KV缓存 | 在线量化,支持4-bit, 8-bit,低精度(4-bit)下效果优于SmoothQuant |
SpinQuant | 权重、激励、KV缓存 | 在线量化,支持4-bit, 8-bit,低精度(4-bit)下效果优于SmoothQuant,GPTQ,量化速度较快,但略慢于GPTQ |
QQQ | 权重、激励 | 在线量化,支持4-bit, 8-bit,推理吞吐较快 |
QoQ | 权重、激励、KV缓存 | 在线量化,支持4-bit, 8-bit,推理吞吐较快 |
FP8 | 权重、激励、KV缓存 | 在线量化,支持FP8精度,依赖较新GPU,推理吞吐较快 |
综上,文章简要介绍了近期一些LLM后量化的算法和方案,当然还有众多算法未涉及和细讲,如SpQR,ZeroQuant, KIVI**,Atom, OmniQuant,AQLM等。
五
参考文献
- GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers
- AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration
- AWQ slides: hanlab.mit.edu/projects/aw…
- SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models
- QuIP: 2-Bit Quantization of Large Language Models With Guarantees
- HQQ: Half-Quadratic Quantization of Large Machine Learning Models.
- QuaRot: Outlier-Free 4-Bit Inference in Rotated LLMs
- SpinQuant: LLM quantization with learned rotations
- QQQ: Quality Quattuor-Bit Quantization for Large Language Models
- QServe: W4A8KV4 Quantization and System Co-design for Efficient LLM Serving
- Nvidia: FP8 Formats for Deep Learning
- FP8量化原理简介:zhuanlan.zhihu.com/p/574825662
- Nvidia Transformer Engine: Using FP8 with Transformer Engine
往期回顾
4.得物 iOS 启动优化之 Building Closure
文 / 旭囧
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
得物可观测平台架构升级:基于GreptimeDB的全新监控体系实践
一、摘 要
在前端可观测分析场景中,需要实时观测并处理多地、多环境的运行情况,以保障 Web 应用和移动端的可用性与性能。传统方案往往依赖代理 Agent → 消息队列 → 流计算引擎 → OLAP 存储的多级架构,虽然能满足基本数据接入与查询需求,但面临以下困难与挑战:
分层架构的精细化演进:
当前分层模型在支撑多业务场景时,需要为分钟级、小时级、天级等不同时间粒度的数据视图分别构建计算链路。这种模式在保障灵活性的同时,可能存在存储与计算资源的重复消耗现象,如何通过增量计算或动态视图技术实现"一次计算多级复用",或将成为提升资源利用效率的关键突破点。
复杂分析场景的效能平衡:
在应对全量 Join、多维交叉分析等高复杂度场景时,现有的按量计费机制与计算资源调度策略,可能在高频业务周期(如大促活动)中面临成本曲线的非线性增长挑战。探索预计算加速、智能冷热分层与弹性资源调度的深度结合,或许能进一步释放大规模分析场景的性价比潜力。
生产环境的安全加固:
基于 SQL 的敏捷开发模式在提升数据处理效率的同时,也对企业级数据资产管理提出了更高要求。通过强化语法预校验、分区保护机制、操作审计追溯等防护手段,构建覆盖开发、测试、发布全流程的可靠性护城河,将有效降低误操作风险并提升数据治理成熟度。
本文聚焦将前端可观测后端数据分析场景演进到 GreptimeDB 的实践,深入剖析如何利用 GreptimeDB Flow 引擎实现 10s、1m、10m 等多粒度持续聚合,结合HyperLogLog 与 UDDsketch 函数,为前端可观测场景提供高性能、低成本且易于运维的端到端解决方案。
二、部署架构
为解决前述痛点,逐步将时序/观测数据场景迁移至 GreptimeDB,并借助其内置的Flow引擎(SQL)自动维护秒级、分钟级、小时级等多精度下采样表,可极大简化分层建模和物化视图运维。
GreptimeDB分布式架构
采用 GreptimeDB 开源的分布式模式,在这种模式下,GreptimeDB 的节点可以分为如下角色:
-
Frontend:负责协议处理、请求校验和鉴权、初步查询优化,是一个无状态节点,可以根据负载任意扩缩容。
-
Datanode:负责管理数据分片、处理数据写入和持久化以及执行具体的查询。
-
Flownode:对于配置了流计算任务的集群,Flownode 负责接受 Frontend 镜像而来的写入请求并执行流计算任务。流计算的结果最终会被写入到 Datanode 中进行持久化。
-
Metasrv:GreptimeDB 的控制面组件,负责管理集群的元数据(如表的分片路由信息等)。Metasrv 本身是无状态的,这里我们采用 PostgreSQL 作为后端存储。
透明数据缓存
GreptimeDB 对数据访问层进行了高度的抽象,负责管理数据分片的 Datanode 并不需要感知到数据文件位于本地磁盘还是对象存储。但是考虑到当使用对象存储时数据文件的访问延迟会大大增加,因此 GreptimeDB 设计了多层的透明数据缓存来解决此问题。
(GreptimeDB 的缓存结构)
GreptimeDB 的缓存结构如上图所示。从缓存所在位置可以分为磁盘缓存和内存缓存两类:
-
磁盘缓存的数据来源通常是对象存储,其类似于操作系统的 page cache,只不过 page cache 是利用内存加速磁盘数据的访问,而 GreptimeDB 的这部分缓存则是利用磁盘加速对象存储的访问,将频繁访问的文件按范围缓存到磁盘可以实现更低的查询延迟,并且能够智能根据访问模式实现预取(prefetch)、IO 合并等优化。
-
内存缓存除了原始的文件内容之外也包括从磁盘/对象存储的原始内容反序列化出来的数据结构,如字段的 min/max,bloomfilter 等等。
而从数据类型来分可以分为结构化和非结构化两类:
-
非结构化缓存的内容通常是文件的二进制内容,而缓存的 key 则是文件名加上字节范围。比如在 flush 的过程中写入到对象存储的文件往往是大概率很快就会被查询的热数据,因此可以在本地缓存一份避免查询请求穿透到对象存储。
-
结构化缓存则是文件、索引的内容或元数据反序列化得到的结构体,这些数据在查询剪枝时频繁被用到,因此 GreptimeDB 缓存了反序列化之后的结构,避免频繁反序列化带来的开销。
尽管 GreptimeDB 的缓存机制较为复杂,但是用户无需过多了解细节,只需要给定特定的缓存大小,GreptimeDB 会自动分配各类缓存的配额以及管理缓存的分配和释放,具体调优指南请参考附录[1]。
无畏扩缩容
GreptimeDB 的最小数据读写单元是表的数据分片(称之为 region)。Region 可以在不同的节点之间进行迁移。目前开源版本的 GreptimeDB 支持手动通过 migrate_region
函数进行 region 的迁移(详见附录[3])。当监控发现某些 datanode 的负载较高时,可以将部分 region 迁移到其他较为空闲的 datanode 上避免可用性的降级。
此外,GreptimeDB 是面向云原生基础设施设计的数据库,其 Metasrv 节点能够实时采集各个节点的负载并且将流量在不同节点之间进行分配。对于不同的负载读写特性,还可以利用 Kubernetes 的弹性调度特性来调整不同节点组的副本数量来实现读写分离。关于 GreptimeDB 读写分离的实践,可以参考附录[2]。
三、GreptimeDB Flow 流计算实践
GreptimeDB Flow 是一个专为时序场景设计的轻量级流计算引擎。 它特别适用于提取 - 转换 - 加载 (ETL) 过程或执行即时的过滤、计算和查询,例如求和、平均值和其他聚合。通过在 Frontend 将写入流量镜像一份到 Flownode 进行计算再写回 Frontend 并进行持久化,它可以确保数据被增量和连续地处理,根据到达的新的流数据更新最终结果。
更加重要的是,编写一个 Flow 流计算任务无需额外的学习成本,它完全使用 SQL 语句定义计算任务。如以下语句:
定义了一个名叫 ngx_status_count的任务,它负责流式地统计 ngx_access_log表中每分钟内每个不同状态码的访问日志数量。在AS之后的任务定义部分是一个标准的 SQL,因此对于熟悉 SQL 的开发者来说极容易上手。
多级持续聚合架构
10s 粒度热数据层
CREATE FLOW rpc_cost_10sSINK TO rpc_cost_10s_aggEXPIRE AFTER '12hours'::INTERVALAS SELECT app_name, url, date_bin('10s'::INTERVAL, timestamp) AS time_window, uddsketch(cost_time_ms, 0.01, 0.001) AS cost_sketchFROM rpc_cost_timeGROUP BY app_name, url, date_bin('10s'::INTERVAL, timestamp);
说明:每 10s 计算一次 UDDsketch,近似捕获耗时分布,并写入“热表”,支持毫秒级查询。
1m 粒度中层 Roll‑up
CREATE FLOW rpc_cost_1mSINK TO rpc_cost_1m_aggEXPIRE AFTER '30days'::INTERVALAS SELECT app_name, url, date_bin('1m'::INTERVAL, time_window) AS time_window_1m, uddsketch_merge(cost_sketch) AS cost_sketch_1mFROM rpc_cost_10s_aggGROUP BY app_name, url, date_bin('1m'::INTERVAL, time_window);
说明:周期性合并 10s 粒度的 sketch,生成分钟级聚合,保留 30 天。
10m 粒度冷层
CREATE FLOW rpc_cost_10mSINK TO rpc_cost_10m_aggEXPIRE AFTER '180days'::INTERVALAS SELECT app_name, url, date_bin('10m'::INTERVAL, time_window_1m) AS time_window_10m, uddsketch_merge(cost_sketch_1m) AS cost_sketch_10mFROM rpc_cost_1m_aggGROUP BY app_name, url, date_bin('10m'::INTERVAL, time_window_1m);
说明:进一步合并至 10 分钟级,存入低成本对象存储,保留 180 天。
UV 近似统计:HyperLogLog
和耗时分布统计类似,统计各个 URL 的独立访问量(UV)也是常见的需求。不过想要精确统计特定时间段的 UV 成本是极高的,因此业界往往使用近似算法来实现 UV 计算,如 HyperLogLog。GreptimeDB v0.12 提供了对 HyperLogLog 相关函数的支持,结合 Flow 可以实现强大的任意时间段 UV 近似统计。
10s UV 状态
CREATE FLOW uv_hll_10sSINK TO uv_state_10sEXPIRE AFTER '12hours'::INTERVALAS SELECT app_name, url, date_bin('10s'::INTERVAL, ts) AS time_window, hll(user_id) AS uv_stateFROM access_logGROUP BY app_name, url, date_bin('10s'::INTERVAL, ts);
-
hll 函数: Flow 任务中我们通过 hll 函数将同一时间窗口内的 user_id 进行散列并写入到 uv_state_10s 的 uv_state字段中。
-
uv_state BINARY 类型: 是一个二进制字段(BINARY 类型),无法直接进行查询。如果要查询某个10 秒的时间窗口内的独立访问用户量,需要通过 hll_count函数来进行查询。
SELECT `app_name`, `url`, hll_count(`uv_state`) as uv_countFROM uv_state_10sWHERE time_window = 1743479260;
1m UV 聚合
- 如果用户需要进一步将 10 秒的访问数据聚合到 1 分钟或者直接需要查询特定时间段内的用户访问数量,则可以通过hll_merge 函数来对二进制的 HyperLogLog 状态进行合并。
CREATE FLOW uv_hll_1mSINK TO uv_state_1mEXPIRE AFTER '180days'::INTERVALAS SELECT app_name, url, date_bin('1m'::INTERVAL, time_window) AS time_window_1m, hll_merge(uv_state) AS uv_stateFROM uv_state_10sGROUP BY app_name, url, date_bin('1m'::INTERVAL, time_window);
- 查询示例:
SELECT app_name, url, hll_count(uv_state) AS uv_countFROM uv_state_1mWHERE time_window_1m = '2025-04-20T15:23:00Z';GROUP BY app_name, url;
效果与收益
- 查询性能显著提升:
预聚合 + 多级 Roll‑up,避免全量扫描,P99 查询延迟从秒级降至毫秒级。
- 存储与成本可控:
不同粒度数据设置差异化 TTL:10s 热表保留 1 天,1m 中表保留 7 天,10m 冷表保留 180 天,冷热分离降低存储成本。
- 资源解偶 & 弹性扩缩容:
Frontend、Flownode、Datanode 独立伸缩,流计算、存储、查询三者互不干扰。
- 开发效率提升:
Flow 编写使用标准 SQL,上手难度低,Roll‑up、HyperLogLog、UDDsketch 等内置函数无需额外学习曲线。
四、最佳实践与落地建议
-
合理划分数据分层:根据监控场景与 SLA 要求确定不同粒度保留策略。
-
调整 sketch 精度:UDDsketch 支持自定义误差范围(α、β 参数),可根据业务侧对 P50/P99 精度要求调优。
-
监控与告警:为各级聚合任务配置失败重试与告警机制,确保持续计算的稳定性。
-
资源规划:根据写入 QPS 与聚合复杂度合理预估 Flownode 与 Datanode 数量,结合对象存储带宽设计分区策略。
参考文档:
1.** [性能调优技巧 | GreptimeDB Documentation | Unified Time-Series Database]
2.** 【使用指南】在 Kubernetes 上部署读写分离的 GreptimeDB 集群
3.Region Migration | GreptimeDB Documentation | Unified Time-Series Database
往期回顾
1. 得物新一代可观测性架构:海量数据下的存算分离设计与实践
文 / 南风
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
得物业务参数配置中心架构综述
一、背景
现状与痛点
在目前互联网飞速发展的今天,企业对用人的要求越来越高,尤其是后端的开发同学大部分精力都要投入在对复杂需求的处理,以及代码架构,稳定性的工作中,在对比下,简单且重复的CRUD就显得更加浪费开发资源。目前scm供应链管理页面中,存在约77%的标准页面,这些标准页面里,还存在着很多类似的参数配置页面,就是对某一个模型进行增、删、改、查、导入、导出进行类似的操作,这种开发工作技术含量较低,而且相对耗费人力。
什么是业务参数配置中心
参数配置中心,是一个能够通过配置的方式,快速生成前端页面以及配套增、删、改、查、导入、导出服务的配置平台,它与得物内部低代码前端页面平台wizard相互集成,参数配置中心提供后台增删改查服务,wizard输出对应的前端页面代码,并可以支持用户自定义修改。
使用场景
- 针对读多写少的简单的单表的增删改查;
- 业务中需要交给运营来修改的复杂ark配置(简单配置除外),可以尝试使用业务参数配置中心接入,减少人为修改JSON可能产生的错误,导致系统无法编译进而产生故障。
比如如下的JSON:
[{"position":"1","red":2.49,"blue":2.4,"green":1},{"position":"2","red":2.49,"blue":2.4,"green":1},{"position":"3","red":2.49,"blue":2.4,"green":1},{"position":"4","red":2.49,"blue":2.4,"green":1},{"position":"5","red":2.49,"blue":2.4,"green":1},{"position":"6","red":2.49,"blue":2.4,"green":1},{"position":"7","red":2.49,"blue":2.4,"green":1},{"position":"8","red":2.49,"blue":2.4,"green":1}]
业务参数配置中心极速体验
-
后台服务搭建流程,以及数据录入
-
数据读取可以通过参数配置中心的SDK,输入自己的业务入参以及自己的业务出参,SDK会自动根据方案下的参数以及用户的输入条件,查询出对应的参数信息:
从上面的快速体验里可以看到很多名词,你一定有会有下面的疑问:
二、整体架构与原理
实现思路
首先我们对这种普通的页面进行初步剖析:页面中总体包含搜索条件、静态展示字段以及操作栏,搜索条件一般是静态字段的子集,并且操作栏的功能一般都类似,所以为了能够结构化地构造出这样的页面,我们可以将静态展示字段进行进一步抽象:比如元素、维度、参数、方案、参数实例。
元素
构成页面的每一个业务字段,统称元素,因为有些字段是大家常用的(比如仓库,品牌,一级类目,省份等),它有自己的字段名称,以及取值范围。
维度
一条记录一定有能够标注其唯一性的信息,可能是一个字段或者是多个字段,在参数中心里,能确定一条记录唯一性的所有字段就叫做维度,维度这个概念在参数中心里很重要,它是不可变的。
参数
在业务发展过程里,可以改变值的字段,就叫参数,也可以说一条记录里,除了维度,都可以叫做参数。
综合维度和参数,举个例子,比如商品信息,商品ID就是维度,商品售价、折扣率就是参数。或者医院挂号系统,科室ID就是维度,挂号费,出诊时间就是参数。
方案
一个参数方案它管理着一个场景下的业务配置,可以简单理解一个方案就代表着一个页面,包含了上述我们说的维度以及参数,并且指定了可以指定哪些字段为搜索条件,哪些是必填字段,哪些字段可以多选。
参数实例
描述好方案并生成页面后,实际产生的业务配置数据,我们称之为参数实例。
经过刚才对页面元素的解剖,大家会发现搭建一个这样的页面,犹如建房子一样,维度与参数是最基础的木料,创建方案就是设计建造的过程,参数实例就是一个个真实的房间,所以业务参数配置中心整体产品思路如下:
整体架构
通过上文的介绍,我们介绍了业务参数配置中心最核心的概念,接下来我们看看整体的架构设计。我们针对这些最核心的概念,来设计实现这些业务功能的架构、核心包含领域模型、领域服务、应用服务以及基础设施层需要的存储部件,以及外部可以整合的导入导出框架、日志框架(外部依赖的框架也可以自己实现)、核心的元素维护、方案维护,存储设计好之后,我们就需要一个SDK,可以让用户访问到我们的数据。
系统的实体关系图如下:
通过上文我们可以初步了解到整体的架构设计,那么每一个子模块我们如何实现?接下来我们分析更加细节的原理。
核心原理
如何设计存储的细节是这个系统的一大挑战,因为既要兼顾页面的灵活变动,也要兼顾数据整体的一致性不受影响,同时也要兼顾整体数据的查询性能,下面的小节列出了所有这些核心的挑战点。
存储流程
每一个页面的字段都不一样,我们是怎么存储的?
从上面的两个页面可以看到,因为页面的字段变化多端,所以我们的思考是,必须采用抽象存储的方式来应对,核心用一张 大宽表存储,其中包含很多抽象列,每一个抽象列在不同的方案下,业务含义不同。
同时把方案的元数据:维度、参数、以及功能性设置(如每个字段是否可以删除,是否需要多选)单独存储,每个方案下的大宽表里的抽象列的业务含义,就存储在这些元数据表中。
同时为了应对大批量的查询,我们引入了OLAP的数据库,对于在应用内部的单点查询,我们走MySQL实现,如果运营后台针对某个字段做大批量查询,则可以用OLAP数据库来缓解查询压力。
下面是存储的整个过程以及举例:
SDK查询流程
因为在业务参数使用时,各个业务方有自己的业务对象,所以我们在SDK中集成了反射的能力,可以避免用户直接感知到底层的抽象存储,查询的流程使用上比较简单,一共分为三步,第一步为自定义request,第二步自定义response,第三步调用SDK方法获取参数实例,比如:
- 定义request:
@Data
public class PinkDeviceCameraConfigRequest implements Serializable {
*/***
* 配置类型
*/
private String configType;
*/***
* 设备编号
*/
private String deviceNo;
}
2. 定义response
@Data
public class PinkDeviceCameraConfigResponse implements Serializable {
*/***
* 配置类型
*/
private String configType;
*/***
* 设备编号
*/
private String deviceNo;
*/***
* 配置明细
*/
private List<CameraConfigDto> configValueList;
@Data
public static class CameraConfigDto implements Serializable {
private String position;
*/***
* 白平衡(Red)
*/
private BigDecimal red;
*/***
* 白平衡(Blue)
*/
private BigDecimal blue;
*/***
* 白平衡(Green)
*/
private BigDecimal green;
*/***
* 亮度(Brightness)
*/
private BigDecimal brightness;
*/***
* 自动曝光时间上限(us)
*/
private BigDecimal autoExposureTimeUpperLimit;
*/***
* 采集帧率
*/
private BigDecimal acquisitionFrameRate;
*/***
* 增益自动开关(us)
*/
private String gainAuto;
*/***
* 增益自动上限
*/
private BigDecimal gainAutoUpperLimit;
*/***
* 增益自动上限
*/
private BigDecimal gainAutoLowerLimit;
}
}
3. 调用SDK的服务方法查询
PinkDeviceCameraConfigRequest pinkDeviceCameraConfigRequest = new PinkDeviceCameraConfigRequest();
pinkDeviceCameraConfigRequest.setConfigType("DEVICE_NO");
pinkDeviceCameraConfigRequest.setDeviceNo("123@LuSun");
*//* 单个查询场景
PinkDeviceCameraConfigResponse response =
paramInstQueryService.getParams("P80-DEVICE-CAMERA-PARAM-MANAGER",
pinkDeviceCameraConfigRequest,
PinkDeviceCameraConfigResponse.class);
*//* 批量查询场景
PageQueryOption pageQueryOption = new PageQueryOption();
pageQueryOption.setPageIndex(1);
pageQueryOption.setPageSize(200);
PageInfo<PinkDeviceCameraConfigResponse> paramsPage =
paramInstQueryService.getParamsPage("P80-DEVICE-CAMERA-PARAM-MANAGER",
pinkDeviceCameraConfigRequest,
PinkDeviceCameraConfigResponse.class,
pageQueryOption);
4. 获得结果
整体查询实现原理如下:
目前整个服务的性能在10+ms左右:
参数优先级实现
为什么会有参数优先级这个功能?
比如有一个场景,要维护一个供应链系统中的补货参数:安全库存,低于这个安全库存的时候,要通知商家进行补货,整个供应链里有100个仓库,20个一级类目,200个二级类目,2000个三级类目,涉及到500个品牌,要维护每一个商品的安全库存,你会怎么实现?
你一定不会把 100仓库2000类目500品牌 = 1000000000种可能全都设置一遍参数,对你来说,重点类目,要单独详细配置安全库存,非重点类目可能只需要管控到一级或者二级类目即可,这样你所需要的配置会大大减少。那么参数的决策就需要遵循一定的规则,比如:
有仓库+一级类目+二级类目+三级类目 的安全库存,优先取;
如果取不到,则取仓库+一级类目+二级类目的安全库存;
再取不到,取仓库+一级类目的安全库存。
比如:
DN仓 鞋 安全库存 100
DN仓 鞋-运动鞋 安全库存 500
DN仓 鞋-运动鞋-篮球鞋 安全库存 1000
那如果一个商品是篮球鞋的话,则会命中安全库存1000的规则,如果是登山鞋的话,只能命中运动鞋的规则取500,如果是高跟鞋,则只能取100的安全库存。
(事实上这种补货规则要详细的多,这里只是方便大家理解需求,并不是真正的参数)
也就是说,当用户的入参同时可能命中多条参数的时候,需要通过优先级来判断应该返回哪个参数。
为了加速查询,系统在设计时添加了两层缓存:
当后台数据发生变化时,会将对应的缓存进行失效。
元素多选处理
维度多选场景:
参数多选场景:
既要保证维度唯一,又要保证能正常搜索,以及展示,如何实现?业务参数配置中心引入了一个“组”的概念,是将同属于一行的参数实例,归为一个组,这个组是最小的新建、编辑单位。
对于新增流程如下图所示:
对于修改流程,如下图所示:
元素范围查询
页面中的字段,我们统称为元素,只要是字段,一定有它的取值范围,我们平衡了用户使用成本以及系统性能,将字段取值类型划分成了四种:
1)枚举类元素
2)dubbo全量接口元素
3)dubbo单点查询接口元素
4)自定义文本元素
-
枚举元素由用户手动在页面创建,一般几十个以内为佳,创建成本不高,比如经常用到的 “是”,“否”,或者比如单据类型等等。
-
dubbo全量接口元素,一般是几十到上百个的体量,比如一级类目,仓库等,地址。
-
dubbo单点查询接口,一般是几千到几万体量的取值范围,无法直接在内存里存储所有枚举,比如品牌等。只能通过两个接口来完成搜索以及数据的展示,比如“品牌ID >品牌名称”接口 和 “品牌名称->品牌ID” 接口。
-
自定义文本,非枚举类字段,可以选择使用自定义文本来承接。
比如以下是可以通过dubbo接口全量获取配置的元素:
与dubbo全量接口的录入类似,单点搜索接口与全量接口不同的点在于,单点接口需要保留一个变量,给系统查询时调用,比如“通过品牌ID 查询品牌名称” 和 “通过品牌名称查询品牌ID” ,需要留给系统调用的入参,用#{var}代替。
当然,有时元素的范围并不是只取决于它自己,可能也取决于同页面里其他元素的取值,比如说有一个质量原因的字段,当一级类目为鞋时 取值为A、B、C,为服装时为 D、E、F,这是元素范围在设置时,就需要将对应的元素入参维护到其中,比如:
接口入参类型 | 接口入参取值 |
---|---|
com.d.s.q.s.d.r.ConfigRequest | {"ruleVersion":#{ruleVersion},"spuId":#{spuId}} |
导入导出
以下是导入处理流程:
为了照顾使用人员的体验,再多数导入场景时,我们的导入文件都用的是文案,而不是后台存储的数值,比如导入的字段包含类目时,导入文件输入的是鞋、服装、美妆等文案,而不是2、3、4这样存储在后台的数值,那么势必这里就会有将文案转换成数值的过程,这其中就用到了2.3.5章节中提到的元素范围查询使用的接口,当然,对于需要其他元素作为入参的元素,我们默认每个元素左边的元素都可以作为当前元素的入参。
业务参数配置中心不适合做什么?
-
有极为复杂的UI交互
-
较为复杂的校验逻辑(长期计划支持)
-
高频写入场景
-
应用查询参数时以非“=”条件匹配
三、总结与展望
本文简要描述了业务参数配置中心的设计思路,参数配置中心配套生成增、删、改、查、导入、导出服务,并且结合前端低代码平台自动生成前端代码,平台目前业务参数中心已经有40+个场景接入节省了大量的工作人日,能够让研发人员,摆脱低效的CRUD,更专注于自己内部业务逻辑的开发。
对于目前系统的未来规划:
-
持续增加SDK的查询灵活性:包括不限于批量代参数优先级对数据进行查询、通过SDK分页查询全量参数、对系统字段吐出方便业务方使用;
-
持续增加对方案定义的灵活性:支持更多的元素范围的定义,比如HTTP等调用方式;
-
持续增加对元数据定义的灵活性:部分元数据的取值可能需要同页面中的另一个元素的取值来决定,所以在取值渲染时,可以保留给其他元素的占位符,进而随着页面的动态变动,后台取值也可以动态变动。
往期回顾
4. 如何合理规划Elasticsearch的索引|得物技术
文 / sakuta
关注得物技术,每周新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。