Qwen3-ASR 从 PyTorch 迁移到 vLLM:一次信创环境下的推理路径改造实录

一、模型没有变,为什么还要更换推理路径?

在 Qwen3-ASR 接入会议系统的早期阶段,熙瑾会悟采用的是相对直接的 PyTorch 路径:通过 Qwen3ASRModel.from_pretrained() 加载模型,收到音频文件后调用 model.transcribe(),再把识别结果返回给上层业务。

这种方式代码直观、调试方便,也比较适合模型验证和单机功能测试。Qwen3-ASR 官方工具包同时提供 Transformers 和 vLLM 两种后端,其中 Transformers 路径可以直接通过 from_pretrained() 使用,vLLM 路径则通过 Qwen3ASRModel.LLM() 或独立服务启动。

但当 Qwen3-ASR 真正进入会悟的会议转写链路后,问题逐渐从"能不能跑"转向"能不能长时间稳定跑":

  • 单路短音频识别正常,并发增加后吞吐提升有限;
  • 音频长度变化较大时,设备利用率波动明显;
  • Python、PyTorch、硬件插件和算子库之间的版本耦合较强;
  • 在部分国产加速卡上,模型可以加载,但部分算子没有进入理想的优化路径;
  • 流式转写还需要重新设计一套推理和状态管理逻辑;
  • 设备厂商提供的生产镜像、模型案例和调优文档更多集中在 vLLM 路径。

最终,我们决定保留 Qwen3-ASR 模型和现有业务接口,只把底层推理方式从 PyTorch 路径迁移到 vLLM 路径。

这次改造的重点不是"换模型",而是给同一个模型更换一套更适合服务化运行的推理底座。

二、为什么信创环境更容易出现两条路径表现不一致

在标准 NVIDIA CUDA 环境中,PyTorch 生态成熟,很多模型使用原生 Transformers 路径就能获得不错的运行效果。

但在信创环境中,硬件通常还需要配套的软件栈:

text 复制代码
国产加速卡
  ↓
设备驱动与运行时
  ↓
PyTorch适配插件
  ↓
算子库、图编译器与通信库
  ↓
Transformers或vLLM
  ↓
Qwen3-ASR

理论上,PyTorch 和 vLLM 最终都会调用底层加速卡完成计算;但工程上,两条路径的适配成熟度未必一致。

在我们接触的部分信创软硬件环境中,PyTorch 路径更偏向"模型能够运行",而 vLLM 路径已经开始围绕在线服务、动态批处理、图模式、内存管理和模型案例进行专项优化。

这种差异并不意味着国产加速卡不支持 PyTorch,也不能概括所有信创产品。更准确的说法是:

部分信创厂商将主要的模型服务化优化放在 vLLM 及其硬件插件上,PyTorch 路径则更适合作为基础兼容和功能验证方案。

昇腾的公开文档已经为 Qwen3-ASR 提供了独立的 vLLM-Ascend 部署教程、镜像要求、启动参数、性能评估方法和硬件相关配置,并要求 vLLM 与硬件插件版本保持匹配。这个现象说明,在部分国产算力平台上,vLLM 已经不只是通用开源框架,而是厂商重点维护的模型服务入口。

因此,迁移前我们先确定了一条原则:

text 复制代码
PyTorch路径:保留,作为验证和回滚方案
vLLM路径:作为正式在线服务路径

三、改造前的 PyTorch 路径

原来的模型加载方式比较直接:

python 复制代码
import os

import torch
from qwen_asr import Qwen3ASRModel

MODEL_PATH = os.getenv(
    "ASR_MODEL_PATH",
    "/data/models/Qwen3-ASR-0.6B",
)

model = Qwen3ASRModel.from_pretrained(
    MODEL_PATH,
    dtype=torch.bfloat16,
    device_map="cuda:0",
    max_inference_batch_size=1,
    max_new_tokens=1024,
)

识别时直接调用:

python 复制代码
def transcribe_with_pytorch(
    audio_path: str,
    language: str | None = "Chinese",
) -> dict:
    results = model.transcribe(
        audio=audio_path,
        language=language,
    )

    if not results:
        raise RuntimeError("ASR模型未返回结果")

    result = results[0]

    return {
        "language": result.language,
        "text": result.text,
    }

再用 FastAPI 封装:

python 复制代码
import asyncio

from fastapi import FastAPI

app = FastAPI()

inference_lock = asyncio.Lock()


@app.post("/asr")
async def transcribe(audio_path: str):
    async with inference_lock:
        result = await asyncio.to_thread(
            transcribe_with_pytorch,
            audio_path,
            "Chinese",
        )

    return result

这条路径的优势很明显:

维度 PyTorch 路径表现
代码理解 简单,调用关系清晰
单步调试 方便,可以直接断点
模型修改 容易修改前向过程
功能验证 适合快速验证模型
依赖数量 相对较少
服务化能力 需要自行补充
高并发调度 需要自行设计
流式识别 需要额外实现

对于单路文件转写,这种方式完全可以使用。问题主要出现在长期服务化运行之后。

四、PyTorch 路径在信创设备上的几个实际问题

1. 能运行,不代表所有算子都进入最佳路径

模型可以完成识别,只能证明功能链路打通。

如果部分算子发生回退、频繁执行设备与主机之间的数据交换,或者没有进入图编译和融合算子路径,最终表现可能是:

text 复制代码
设备利用率时高时低
单路延迟尚可
并发增加后吞吐增长有限
长音频波动明显
CPU占用偏高
Python调用栈较重

这类问题通常不会直接报错,却会影响生产环境中的稳定性和容量规划。

2. 版本组合过多

PyTorch 路径通常同时依赖:

text 复制代码
Python
PyTorch
Transformers
qwen-asr
硬件PyTorch插件
驱动
运行时
算子库
FlashAttention或替代实现

某个组件升级后,可能出现模型能导入但推理失败、符号找不到、算子不支持或显存占用变化。

3. 并发需要业务层自己管理

为了避免显存溢出,原服务使用了全局锁:

python 复制代码
inference_lock = asyncio.Lock()

这可以保证稳定,但也意味着多个请求最终仍然排队串行执行。即使设备还有计算余量,业务层也难以安全地自行拼接动态批次。

4. 流式能力难以平滑接入

Qwen3-ASR 官方目前只在 vLLM 后端提供流式推理,流式模式不支持批量推理,也不返回时间戳。也就是说,继续停留在 PyTorch 路径,需要自行补齐流式状态、音频分块和增量结果管理。

五、迁移目标:业务接口保持不变

我们不希望上层会议业务感知底层推理框架变化。

改造前后的链路设计如下:

text 复制代码
改造前:

会议音频
  ↓
ASR业务服务
  ↓
PyTorch模型进程
  ↓
转写结果


改造后:

会议音频
  ↓
ASR业务服务
  ↓
vLLM推理服务
  ↓
OpenAI兼容响应
  ↓
统一转写结果

会悟仍然调用原来的内部 ASR 接口:

text 复制代码
POST /api/asr/transcribe

返回结构也保持不变:

json 复制代码
{
  "language": "Chinese",
  "text": "本次会议主要讨论推理后端迁移方案。",
  "duration_sec": 65.38,
  "process_sec": 4.21,
  "rtf": 0.0644
}

这样做有两个好处:

第一,会议录音、说话人处理、纪要生成和资料归档模块不需要同步修改。

第二,vLLM 出现兼容问题时,可以快速切回原来的 PyTorch 服务。

六、先不要删除旧环境,单独创建 vLLM 环境

原来的 PyTorch 环境继续保留:

text 复制代码
/data/envs/qwen3-asr-pytorch

新建 vLLM 环境:

bash 复制代码
conda create \
  -n qwen3-asr-vllm \
  python=3.12 \
  -y

conda activate qwen3-asr-vllm

python -m pip install --upgrade pip
pip install -U "qwen-asr[vllm]"

Qwen3-ASR 官方明确将 vLLM 作为更快的推理后端,并提供 qwen-asr[vllm] 安装方式、Python 调用方式以及 qwen-asr-serve 服务化命令。

在信创环境中,不建议直接执行不带版本限制的安装命令。

更稳妥的做法是优先使用硬件厂商提供的镜像或依赖包:

text 复制代码
硬件驱动版本
运行时版本
PyTorch插件版本
vLLM版本
vLLM硬件插件版本
qwen-asr版本
模型版本

这些版本必须作为一个整体保存。

检查环境:

bash 复制代码
python -V

pip show qwen-asr
pip show vllm

python -c "
import vllm
print(vllm.__version__)
"

如果使用国产加速卡,还要额外检查对应插件:

bash 复制代码
pip list | grep -Ei \
  'ascend|musa|metax|vllm'

七、第一种改法:在 Python 中使用 vLLM 后端

Qwen3-ASR 官方提供了 Qwen3ASRModel.LLM() 初始化方式:

python 复制代码
import os

from qwen_asr import Qwen3ASRModel

MODEL_PATH = os.getenv(
    "ASR_MODEL_PATH",
    "/data/models/Qwen3-ASR-0.6B",
)


def main() -> None:
    model = Qwen3ASRModel.LLM(
        model=MODEL_PATH,
        gpu_memory_utilization=0.65,
        max_inference_batch_size=8,
        max_new_tokens=2048,
    )

    results = model.transcribe(
        audio=[
            "/data/test/meeting_01.wav",
            "/data/test/meeting_02.wav",
        ],
        language=[
            "Chinese",
            "Chinese",
        ],
    )

    for result in results:
        print(result.language)
        print(result.text)


if __name__ == "__main__":
    main()

官方特别提醒,vLLM 初始化代码应放在:

python 复制代码
if __name__ == "__main__":

下面,否则多进程启动方式可能引发 spawn 相关错误。

这条路径对原有代码改动较小,但模型生命周期仍然和业务 Python 进程绑定。

如果业务服务重启,模型也会重新加载;如果 ASR 进程异常,接口服务会一起退出。

因此,生产环境中我们最终选择了第二种方式:把 vLLM 做成独立推理服务。

八、第二种改法:独立启动 vLLM 服务

最简单的启动方式是:

bash 复制代码
export CUDA_VISIBLE_DEVICES=0

qwen-asr-serve \
  /data/models/Qwen3-ASR-0.6B \
  --served-model-name qwen3-asr-0.6b \
  --gpu-memory-utilization 0.65 \
  --max-model-len 4096 \
  --host 127.0.0.1 \
  --port 18081

qwen-asr-serve 是对 vllm serve 的封装,可以继续传递 vLLM 支持的服务参数。Qwen3-ASR 也已经获得 vLLM 的模型支持,可以通过 OpenAI 风格接口调用。

服务启动后检查:

bash 复制代码
curl http://127.0.0.1:18081/v1/models

预期返回:

json 复制代码
{
  "object": "list",
  "data": [
    {
      "id": "qwen3-asr-0.6b",
      "object": "model"
    }
  ]
}

vLLM 可以作为 OpenAI API 兼容服务运行,因此上层系统只需要配置地址、模型名称和认证信息,不必直接依赖 vLLM 的 Python 内部结构。

信创环境下的参数不要直接照搬 CUDA 示例

不同硬件插件支持的参数可能不同。

部分信创环境需要增加:

text 复制代码
--enforce-eager
--dtype float16
--max-model-len 4096
硬件专用additional-config
图编译配置

以 vLLM-Ascend 为例,官方 Qwen3-ASR 部署文档针对不同昇腾设备给出了不同启动参数,并明确要求使用匹配的 vLLM 与 vLLM-Ascend 版本。

因此,通用命令只能作为起点,不能直接当作所有国产加速卡的生产配置。

九、业务适配层如何调用 vLLM

可以通过 OpenAI SDK 发送音频请求:

python 复制代码
import base64
import os
from pathlib import Path

from openai import OpenAI
from qwen_asr import parse_asr_output

VLLM_BASE_URL = os.getenv(
    "VLLM_BASE_URL",
    "http://127.0.0.1:18081/v1",
)

MODEL_NAME = os.getenv(
    "VLLM_MODEL_NAME",
    "qwen3-asr-0.6b",
)

client = OpenAI(
    base_url=VLLM_BASE_URL,
    api_key="EMPTY",
)


def audio_to_data_url(audio_path: str) -> str:
    path = Path(audio_path)
    suffix = path.suffix.lower().lstrip(".") or "wav"

    encoded = base64.b64encode(
        path.read_bytes()
    ).decode("ascii")

    return (
        f"data:audio/{suffix};base64,"
        f"{encoded}"
    )


def transcribe_with_vllm(
    audio_path: str,
) -> dict:
    response = client.chat.completions.create(
        model=MODEL_NAME,
        messages=[
            {
                "role": "user",
                "content": [
                    {
                        "type": "audio_url",
                        "audio_url": {
                            "url": audio_to_data_url(
                                audio_path
                            )
                        },
                    }
                ],
            }
        ],
        temperature=0,
    )

    raw_content = (
        response.choices[0]
        .message.content
    )

    language, text = parse_asr_output(
        raw_content
    )

    return {
        "language": language,
        "text": text,
    }

为了避免大文件经过 Base64 后明显膨胀,也可以由内部文件服务提供受控 URL,再让 vLLM 服务读取。

但在隔离网络或安全敏感环境中,需要特别防范服务端请求伪造问题。vLLM 推理服务不应被允许任意访问办公网、管理网或互联网地址。

更稳妥的方式是:

text 复制代码
业务服务接收文件
  ↓
保存到受控音频目录
  ↓
内部文件服务生成一次性地址
  ↓
vLLM读取指定网段地址
  ↓
任务结束后删除文件

十、保留统一接口,避免业务层绑定 vLLM

业务接口继续使用原来的结构:

python 复制代码
import os
import time

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

ASR_BACKEND = os.getenv(
    "ASR_BACKEND",
    "vllm",
)


class ASRResult(BaseModel):
    backend: str
    language: str
    text: str
    process_sec: float


@app.post("/api/asr/transcribe")
def transcribe(audio_path: str):
    started_at = time.perf_counter()

    if ASR_BACKEND == "pytorch":
        result = transcribe_with_pytorch(
            audio_path
        )
    elif ASR_BACKEND == "vllm":
        result = transcribe_with_vllm(
            audio_path
        )
    else:
        raise ValueError(
            f"不支持的ASR_BACKEND:"
            f"{ASR_BACKEND}"
        )

    return {
        "backend": ASR_BACKEND,
        "language": result["language"],
        "text": result["text"],
        "process_sec": round(
            time.perf_counter() - started_at,
            3,
        ),
    }

切换到 vLLM:

bash 复制代码
export ASR_BACKEND=vllm

回滚到 PyTorch:

bash 复制代码
export ASR_BACKEND=pytorch

这种结构让会悟只依赖统一的 ASR 业务接口,不直接绑定 PyTorch、vLLM 或特定硬件插件。

后续即使再次更换推理后端,也只需要修改适配层。

十一、前后性能测试怎么做

性能对比至少要保证以下条件一致:

text 复制代码
同一台服务器
同一张加速卡
同一个Qwen3-ASR模型
同一批音频
相同采样率和声道
相同语言参数
相同max_new_tokens
相同是否启用时间戳
相同音频预处理流程

vLLM-Ascend 的 Qwen3-ASR 文档也建议至少记录音频时长、请求并发、端到端延迟、实时因子和吞吐量,并分别测试短音频、长音频及并发场景。

测试环境记录模板

项目 测试配置
模型 Qwen3-ASR-0.6B
音频格式 16kHz、单声道、PCM WAV
PyTorch 路径 Transformers + 厂商 PyTorch 插件
vLLM 路径 vLLM + 厂商 vLLM 插件
推理精度 BF16 或 FP16
时间戳 关闭
测试轮数 每组预热 3 次,正式测试 10 次
RTF 口径 处理耗时 ÷ 音频时长

前后性能对比表

下表为脱敏后的演示数据,用于展示文章中的统计方式,不代表任何具体信创硬件的公开性能。

测试项目 PyTorch 路径 vLLM 路径 迁移后变化
模型冷启动时间 44.8 秒 68.5 秒 启动更慢
30 秒音频单路耗时 2.04 秒 1.62 秒 降低 20.6%
5 分钟音频单路耗时 22.7 秒 16.9 秒 降低 25.6%
5 分钟音频 RTF 0.076 0.056 降低 26.3%
4 路 60 秒音频总耗时 27.6 秒 10.8 秒 降低 60.9%
4 路场景音频吞吐 8.7 倍实时 22.2 倍实时 提升约 2.55 倍
空载设备内存占用 6.8 GB 9.6 GB 增加 2.8 GB
并发调度方式 业务锁与固定批次 引擎统一调度 逻辑简化
流式转写 需单独开发 原生支持 接入成本降低

这组结果反映出的趋势是:

vLLM 不一定在所有指标上都占优。

它的模型启动时间更长,空载内存占用也可能更高,因为服务启动时会预留部分设备内存。

真正明显的改善主要出现在:

text 复制代码
中长音频
多个请求并发
任务长度不一致
持续在线服务
流式识别

如果业务始终只有单路短音频,PyTorch 和 vLLM 的差距可能并不明显,甚至 PyTorch 更轻量。

十二、写一个简单的双路径测试脚本

python 复制代码
import csv
import statistics
import time
from pathlib import Path
from typing import Callable


def benchmark_backend(
    name: str,
    transcribe_func: Callable[[str], dict],
    audio_files: list[Path],
    repeat: int = 5,
) -> list[dict]:
    rows = []

    for audio_path in audio_files:
        costs = []

        # 预热一次
        transcribe_func(str(audio_path))

        for _ in range(repeat):
            started_at = time.perf_counter()

            result = transcribe_func(
                str(audio_path)
            )

            cost = (
                time.perf_counter()
                - started_at
            )
            costs.append(cost)

        rows.append(
            {
                "backend": name,
                "filename": audio_path.name,
                "language": result["language"],
                "mean_sec": round(
                    statistics.mean(costs),
                    4,
                ),
                "p50_sec": round(
                    statistics.median(costs),
                    4,
                ),
                "min_sec": round(
                    min(costs),
                    4,
                ),
                "max_sec": round(
                    max(costs),
                    4,
                ),
            }
        )

    return rows


audio_files = sorted(
    Path("/data/asr-benchmark")
    .glob("*.wav")
)

rows = []

rows.extend(
    benchmark_backend(
        "pytorch",
        transcribe_with_pytorch,
        audio_files,
    )
)

rows.extend(
    benchmark_backend(
        "vllm",
        transcribe_with_vllm,
        audio_files,
    )
)

with open(
    "/data/asr-benchmark/result.csv",
    "w",
    encoding="utf-8-sig",
    newline="",
) as file_obj:
    writer = csv.DictWriter(
        file_obj,
        fieldnames=rows[0].keys(),
    )
    writer.writeheader()
    writer.writerows(rows)

测试时不要只执行一次。

模型第一次请求通常包含缓存初始化、图编译或运行时预热。如果把第一次请求直接作为正式结果,很容易误判两条路径的差异。

建议采用:

text 复制代码
冷启动单独记录
预热请求不计入平均值
正式请求至少执行5至10次
同时记录平均值、P50和最大值

十三、迁移过程中最容易踩的几个坑

1. gpu_memory_utilization 不是越高越好

vLLM 会根据该参数规划可使用的设备内存。

bash 复制代码
--gpu-memory-utilization 0.9

不代表一定能获得最高性能。如果同一张卡还运行声纹、VAD或其他模型,设置过高可能导致其他进程无法启动。

共享设备时可以从:

bash 复制代码
--gpu-memory-utilization 0.55

或:

bash 复制代码
--gpu-memory-utilization 0.65

开始测试。

2. vLLM 启动慢不代表推理慢

vLLM 启动阶段需要完成模型加载、内存规划和部分执行准备,因此冷启动通常比直接加载 PyTorch 模型更慢。

生产环境不应频繁启停模型容器,也不要为每个转写任务临时启动一次 vLLM。

3. 硬件插件版本必须和 vLLM 匹配

这是信创环境中最重要的一点。

不能只升级:

bash 复制代码
pip install -U vllm

而不检查厂商插件是否支持这个版本。

昇腾官方文档明确要求使用与 vLLM 版本匹配的 vLLM-Ascend 镜像,并通过支持矩阵确认模型状态。其他国产加速卡环境同样应优先遵循厂商发布的版本组合,而不是只追求最新版。

4. 流式路径和离线文件路径仍需分别测试

Qwen3-ASR 的流式推理当前仅由 vLLM 后端提供,但流式模式不支持批量推理和时间戳。

因此不能因为离线文件识别正常,就认为流式会议也已经完成迁移。

至少需要额外验证:

text 复制代码
首个增量结果延迟
音频分块大小
断流重连
最终文本稳定性
会话状态清理
并发会话数量

5. 时间戳仍可能需要独立服务

Qwen3-ASR 的时间戳由 Forced Aligner 提供。

在实际架构中,可以采用:

text 复制代码
vLLM:负责ASR转写
PyTorch服务:负责Forced Aligner

这并不矛盾。

推理路径迁移不要求把所有相关模型全部塞进同一个 vLLM 服务。对于低频使用的时间戳功能,独立按需调用反而更节省设备资源。

十四、上线时采用双服务灰度,而不是原地覆盖

迁移期间同时保留两个端口:

text 复制代码
PyTorch服务:127.0.0.1:18140
vLLM服务:127.0.0.1:18141

业务配置:

bash 复制代码
ASR_BACKEND=vllm

PYTORCH_ASR_URL=http://127.0.0.1:18140
VLLM_ASR_URL=http://127.0.0.1:18141

灰度顺序:

text 复制代码
第一阶段:同一音频双跑,只使用PyTorch结果
第二阶段:内部测试会议使用vLLM结果
第三阶段:部分普通会议切换到vLLM
第四阶段:扩大到长会议和并发任务
第五阶段:vLLM成为默认路径

每个阶段都记录:

text 复制代码
识别耗时
RTF
失败率
超时次数
设备内存
设备利用率
文本差异
会议纪要生成是否正常

确认稳定后,再停止 PyTorch 常驻服务,但环境、模型和启动脚本仍应保留一段时间。

十五、从迁移结果看,vLLM真正解决了什么

迁移完成后,最明显的变化并不是单条音频快了多少,而是 ASR 模块从"嵌在 Python 业务中的模型"变成了"独立推理服务"。

改造前:

text 复制代码
业务进程负责接口
业务进程负责模型
业务进程负责并发锁
业务进程负责显存控制
业务进程异常时模型一起退出

改造后:

text 复制代码
业务进程负责文件和任务
vLLM负责模型生命周期
vLLM负责请求调度
vLLM负责设备内存规划
两层服务可以独立重启

会悟的上层会议流程没有因为推理框架变化而重新设计,仍然接收统一的转写文本,再继续完成说话人处理、纪要生成、待办提取和资料归档。

十六、总结:信创适配不能只看模型能否启动

Qwen3-ASR 从 PyTorch 路径迁移到 vLLM 路径,本质上是一次从"模型调用"到"推理服务"的改造。

PyTorch 路径并没有失去价值。它仍然适合:

text 复制代码
模型功能验证
算法调试
小规模单路识别
Forced Aligner
故障回滚

vLLM 路径则更适合:

text 复制代码
长期在线服务
多请求调度
中长音频
并发转写
流式识别
标准化API接入

在部分信创环境中,厂商对 vLLM 路径提供了更完整的镜像、插件、模型案例和调优参数,因此同一个 Qwen3-ASR 模型通过 vLLM 运行时,更容易进入经过专项优化的设备执行路径。

但这不是一个可以脱离硬件和版本讨论的绝对结论。真正决定迁移效果的,仍然是固定版本、统一输入、真实会议音频和完整的并发测试。

对熙瑾会悟而言,这次迁移最大的收益不是单项性能数字,而是让 ASR 模块拥有了更清晰的服务边界、更稳定的并发处理方式,以及后续适配不同信创算力平台的空间。

相关推荐
阿童木写作1 小时前
Python实现Temu图片批量翻译自动化教程
运维·人工智能·python·自动化
冬奇Lab2 小时前
代码库知识库系列(06):把调用图编码进 Embedding——结构增强有效,但不够
人工智能
冬奇Lab2 小时前
开源项目第175期:Buzz — Jack Dorsey 的 Block 用 Nostr 重新定义团队协作,AI Agent 拥有自己的加密身份
人工智能·开源·资讯
AI分享猿2 小时前
游戏原画与建筑灵感:AI图像生成如何服务前期设计
人工智能·游戏
字节跳动视频云技术团队3 小时前
为什么 AI 视频,需要“懂生成”的画质增强
人工智能
后端小肥肠3 小时前
我做了个能一键搭建个人工作台的 Skill,已开源
人工智能·aigc·agent
码云之上4 小时前
AI Agent 工程化总览篇:从 Prompt 到 Harness
前端·人工智能
2501_926978334 小时前
以说明书 DNA 为模板——完整 AGI 的结构图景
前端·人工智能·经验分享·笔记·ai写作