阿里云 Milvus AI Function | 低成本和高稳定的双重加强

作者:周弘懿(锦琛)

业务背景

AI 应用上线后,团队很快会撞上三件事:相同的文本被反复向量化、在线推理动辄几秒的延迟、以及突发流量把模型额度瞬间打满。

商品标题会反复同步,客服 FAQ 会多次发布,知识库段落也会因为重试或增量导入再次进入 Embedding 模型------文本没变,向量通常也不变,模型调用和等待时间却实实在在地又发生了一次。

与此同时,很多 AI 任务从一开始就没有「实时返回」的要求。商品描述补全、历史客服会话摘要、内容翻译、知识库初始化,今晚处理、明早交付,完全不影响用户体验。这类任务如果仍然走实时接口,既要按实时价格付费,又会挤占在线搜索与问答的模型额度。

于是 AI 应用的降本诉求可以拆成两条清晰的路径:

  • 相同内容只做一次向量化,把重复的模型调用省下来;

  • 可以等待的任务离线批量处理,用更低的单价完成大批量加工。

同时降本不能以牺牲稳定性为代价。AI 调用一旦进入生产链路,还要直面突发流量、账号限流和局部故障。真正可用的方案,既要压低每次任务的成本,也要在异常发生时保证核心写入与查询不中断。

阿里云 Milvus AI Function 通过 Embedding Cache 与 AI Batch 解决降本,通过内网直连、多账号负载均衡与故障切换、缓存降级 守住稳定,再用 Data Inspection 补上内容安全的最后一道门。

技术挑战

把「降本」和「稳定」同时做好,传统方案会遇到几条绕不开的难点:

  1. 相同内容重复向量化,Token 白花:商品资料、FAQ、热门查询反复进入 Embedding 模型,向量结果完全一样,Token 与等待时间却重复消耗。

  2. 可离线的任务仍走实时接口:历史工单归档、会话摘要、描述补全本可以按天集中处理,却因为没有离线通道而按实时价格计费,还占用在线额度。

  3. AI 调用进入生产链路后连续性难保障:突发流量、单账号限流、API Key 异常都可能让一次写入或查询失败,而 AI 调用往往就挂在核心链路上。

  4. 缓存本身可能成为硬依赖:为降本引入的缓存,一旦 Redis 抖动或读取超时,反而会拖慢甚至拖垮主链路。

  5. 内容安全是上线门槛:用户输入可能含风险内容,模型输出也可能不适合直接交付,缺了护栏产品过不了合规评审。

解决方案

阿里云 Milvus AI Function 把上面几件事收敛进同一套能力里:

能力 作用 解决的问题
Embedding Cache 按文本与调用上下文精确匹配已有向量,命中即复用 相同内容只做一次向量化,省下重复 Token
AI Batch 请求写入 JSONL 异步提交,按实时推理价格 50% 计费 可等待的大批量任务离线低价完成,不挤占在线额度
内网直连 AI Function 与百炼通过内网打通 缩短调用路径,减少公网波动与性能损耗
多账号负载均衡 / 故障切换 多账号分摊调用、异常 Key 自动切换 降低单账号限流、单 Key 异常对连续性的影响
缓存降级 连续慢读 / 错误达阈值即进入降级窗口、回源模型 缓存不拖累主链路,宁可多调模型也不中断
Data Inspection input / output / both 三态内容检查 风险内容进模型前、出模型前各加一道门

具体 AI Function 用法参考官方文档。

整体能力示意如下:

降本维度一:让相同内容只做一次向量化

Embedding Cache 会先按文本和调用上下文查找已有向量,命中后直接复用,只有首次出现的新内容才真正调用模型。缓存采用精确匹配------两段文字只要写法不同,就会分别计算。它的收益完全来自重复率:重复越多,省下的模型请求越多。

以日均 10 万次向量化、平均每次 50 Token、缓存命中率 50%、按 30 天计算,命中的那一半请求不再产生任何模型调用与等待,整体 Token 消耗大约下降一半。

Embedding Cache 更适合在线、高频、重复明显的内容:

  • 客服 FAQ 和标准答案

  • 热门商品标题与商品属性

  • 知识库重复导入的段落

  • 高频搜索词和固定查询模板

降本维度二:把可以等待的任务交给 Batch

Batch 会把请求写入 JSONL 文件后异步提交。百炼 Batch 对支持该能力的模型按实时推理价格的 50% 计费------相同模型、相同 Token 规模下,一笔按实时估算为 100 元的任务,Batch 对应的模型调用费用约为 50 元。

沿用同一口径试算,日均处理 10 万条离线任务、平均输入 50 Token、输出 500 Token,使用 AI Batch 后月调用成本可从约 3,636 元降至 1,818 元。

试算场景 口径 优化前 优化后
Embedding Cache 日均 10 万次、平均 50 Token、命中率 50%、30 天 全量调用模型 重复内容 0 Token,整体约降一半
AI Batch 日均 10 万条、输入 50 / 输出 500 Token、30 天 约 3,636 元 / 月 约 1,818 元 / 月

以上数字用于说明计费比例,实际账单以模型、地域与当期价格为准。

Batch 更适合离线、大批量、允许等待结果的任务:

  • 历史文档生成摘要或标签

  • 存量商品资料批量加工

  • 图片、视频、音频素材生成描述

  • 模型评测与数据标注

  • 周期性重建和夜间任务

聊天回复、搜索补全等即时交互仍应使用实时调用------只要用户正在等待结果,就不该为了折扣把请求放进 Batch。

实战操作

一、用两次相同写入验证 Embedding Cache

下面是一段自包含的可运行代码:第一次写入生成向量并写入缓存,第二次写入相同内容时直接复用。是否命中可观察 Redis keyspace_hits 或 Embedding Provider 的调用次数(两次向量相同只能说明模型输出一致,不能单独证明命中缓存)。

代码参考:

python 复制代码
import json
import time

from pymilvus import DataType, Function, FunctionType, MilvusClient

# ==================== 连接配置 ====================
MILVUS_URI = "http://c-xxx.milvus.aliyuncs.com:19530"  # 端口必须写 19530
MILVUS_TOKEN = "root:xxx"
MODEL_NAME = "text-embedding-v4"
VECTOR_DIM = 1024

client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN)
collection_name = "ai_embedding_cache_demo"

if client.has_collection(collection_name):
    client.drop_collection(collection_name)

schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("content", DataType.VARCHAR, max_length=4096)
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM)
schema.add_function(
    Function(
        name="embed_content_with_cache",
        function_type=FunctionType.TEXTEMBEDDING,
        input_field_names=["content"],
        output_field_names=["embedding"],
        params={
            "provider": "aliyun_milvus",
            "model_name": MODEL_NAME,
            "dim": VECTOR_DIM,
            "cache": json.dumps({
                "enabled": True,
                "exact_cache": {
                    "enabled": True,
                    "backend": "redis",
                    "ttl_hours": 24,
                },
            }),
        },
    )
)

index_params = client.prepare_index_params()
index_params.add_index(
    field_name="embedding",
    index_type="AUTOINDEX",
    metric_type="COSINE",
)
client.create_collection(
    collection_name=collection_name,
    schema=schema,
    index_params=index_params,
)

row = {"content": "Milvus is an open-source vector database."}

print("=" * 64)
print("Embedding Cache 验证:两次写入完全相同的内容")
print("=" * 64)
print(f"写入内容 : {row['content']}")
print(f"缓存配置 : exact_cache backend=redis, ttl=24h")
print("-" * 64)

# 第 1 次写入:缓存未命中,调用模型生成向量并写入缓存
t0 = time.perf_counter()
client.insert(collection_name, [row])
client.flush(collection_name)
t_first = (time.perf_counter() - t0) * 1000
print(f"[第 1 次写入] 缓存未命中 → 调模型生成 embedding   耗时 {t_first:8.1f} ms")

# 第 2 次写入相同内容:缓存命中,复用已有向量,无需再调模型
t0 = time.perf_counter()
client.insert(collection_name, [row])
client.flush(collection_name)
t_second = (time.perf_counter() - t0) * 1000
print(f"[第 2 次写入] 相同内容   → 命中缓存复用向量      耗时 {t_second:8.1f} ms")
if t_second < t_first:
    print(f"           ↳ 第 2 次快 {t_first - t_second:.1f} ms(约 {t_first / t_second:.1f}x)")
print("-" * 64)

# 取出两条记录的向量逐元素比对:命中缓存则两份向量应完全一致
rows = client.query(
    collection_name,
    filter="",
    output_fields=["id", "content", "embedding"],
    limit=10,
)
rows = sorted(rows, key=lambda r: r["id"])
print(f"写入记录数 : {len(rows)}")
for r in rows:
    vec = r["embedding"]
    print(f"  · id={r['id']}  dim={len(vec)}  首 3 维={[round(x, 6) for x in vec[:3]]}")

if len(rows) >= 2:
    v1, v2 = rows[0]["embedding"], rows[1]["embedding"]
    max_diff = max(abs(a - b) for a, b in zip(v1, v2))
    print("-" * 64)
    print(f"两条向量逐元素最大差值 : {max_diff:.3e}")
    if max_diff == 0.0:
        print("结论 : ✅ 两次向量完全一致 ------ 第 2 次确实复用了缓存向量,Embedding Cache 命中")
    else:
        print("结论 : ❌ 两次向量存在差异 ------ 未复用缓存,请检查缓存配置")

示例把有效期设为 24 小时,可按内容更新频率调整:商品标题和 FAQ 更新不频繁时适当延长,内容变化快时缩短,让新版本更快重新计算。缓存不可用或超时时,Milvus 会继续调用模型,业务写入和查询不会因此中断。

二、一个夜间知识库加工的 Batch 例子

一家电商白天把模型额度留给正在搜索和咨询的用户,夜里再集中处理当天积累的商品资料和客服会话:批量生成商品描述、补齐标签、把长对话整理成摘要。第二天运营上班时结果已写回业务系统------用户没有多等一秒,企业也不必按实时价格处理这批数据。

输入文件一行对应一条任务,用 custom_id 关联原始数据:

jsonl 复制代码
{"custom_id":"article-001","method":"POST","url":"/v1/chat/completions","body":{"model":"qwen3.7-max","messages":[{"role":"user","content":"请为这篇知识库文章生成一句话摘要......"}],"enable_thinking":false}}
{"custom_id":"article-002","method":"POST","url":"/v1/chat/completions","body":{"model":"qwen3.7-max","messages":[{"role":"user","content":"请为这篇知识库文章生成一句话摘要......"}],"enable_thinking":false}}

参考代码:

python 复制代码
import json
import os
import shutil
import tempfile
import time
import uuid
from pathlib import Path
from urllib.error import HTTPError
from urllib.request import Request, urlopen

# ==================== 连接配置 ====================
# Milvus AI Batch 走 RESTful 接口,base_url 用实例地址(端口 19530)
MILVUS_BASE_URL = "http://c-xxxx.milvus.aliyuncs.com:19530"
MILVUS_TOKEN = "root:xxx"
MODEL_NAME = "qwen3.7-max"        # 批处理调用的模型,必须与 input.jsonl 里每行 body.model 一致
PROVIDER = "aliyun_milvus"
ENDPOINT = "/v1/chat/completions"


def post_json(path, body, timeout=120):
    request = Request(
        f"{MILVUS_BASE_URL.rstrip('/')}{path}",
        data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {MILVUS_TOKEN}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            return response.status, json.loads(response.read().decode("utf-8"))
    except HTTPError as exc:
        return exc.code, json.loads(exc.read().decode("utf-8"))


def upload_input_file(input_file):
    """multipart 上传 input.jsonl,等价于 OpenAI 的 files.create(purpose='batch')"""
    boundary = f"----milvus-ai-batch-{uuid.uuid4().hex}"
    fields = {
        "provider": PROVIDER,
        "model_name": MODEL_NAME,
        "endpoint": ENDPOINT,
        "purpose": "batch",
    }
    with tempfile.TemporaryFile(mode="w+b") as payload:
        for name, value in fields.items():
            payload.write(f"--{boundary}\r\n".encode())
            payload.write(f'Content-Disposition: form-data; name="{name}"\r\n\r\n'.encode())
            payload.write(value.encode())
            payload.write(b"\r\n")

        payload.write(f"--{boundary}\r\n".encode())
        payload.write(b'Content-Disposition: form-data; name="file"; filename="input.jsonl"\r\n')
        payload.write(b"Content-Type: application/jsonl\r\n\r\n")
        with open(input_file, "rb") as fh:
            shutil.copyfileobj(fh, payload)
        payload.write(b"\r\n")
        payload.write(f"--{boundary}--\r\n".encode())

        length = payload.tell()
        payload.seek(0)
        request = Request(
            f"{MILVUS_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/upload",
            data=payload,
            headers={
                "Authorization": f"Bearer {MILVUS_TOKEN}",
                "Content-Type": f"multipart/form-data; boundary={boundary}",
                "Content-Length": str(length),
            },
            method="POST",
        )
        try:
            with urlopen(request, timeout=600) as response:
                return response.status, json.loads(response.read().decode("utf-8"))
        except HTTPError as exc:
            return exc.code, json.loads(exc.read().decode("utf-8"))


def download_batch_file(batch_id, file_type, output_file):
    """下载结果文件,等价于 OpenAI 的 files.content(output_file_id)"""
    request = Request(
        f"{MILVUS_BASE_URL.rstrip('/')}/v2/vectordb/ai/batch/files/content",
        data=json.dumps(
            {"provider": PROVIDER, "batch_id": batch_id, "file_type": file_type},
            ensure_ascii=False,
        ).encode("utf-8"),
        headers={
            "Authorization": f"Bearer {MILVUS_TOKEN}",
            "Content-Type": "application/json",
        },
        method="POST",
    )
    with urlopen(request, timeout=600) as response, output_file.open("wb") as out:
        shutil.copyfileobj(response, out)


# 1) 上传 JSONL 输入文件
status, data = upload_input_file("input.jsonl")
print(json.dumps(data, ensure_ascii=False, indent=2))
input_file_id = data.get("data", {}).get("id")
if status != 200 or not input_file_id:
    raise SystemExit(f"上传输入文件失败:HTTP={status} code={data.get('code')} message={data.get('message')}")

# 2) 创建 Batch 任务,声明完成窗口
status, data = post_json(
    "/v2/vectordb/ai/batch/jobs/create",
    {
        "provider": PROVIDER,
        "input_file_id": input_file_id,
        "endpoint": ENDPOINT,
        "completion_window": "24h",
    },
)
print(json.dumps(data, ensure_ascii=False, indent=2))
batch_id = data.get("data", {}).get("id")
if status != 200 or not batch_id:
    raise SystemExit(f"创建 Batch 任务失败:HTTP={status} code={data.get('code')} message={data.get('message')}")

# 3) 轮询任务状态
while True:
    status, data = post_json(
        "/v2/vectordb/ai/batch/jobs/describe",
        {"provider": PROVIDER, "batch_id": batch_id},
    )
    batch = data.get("data", {})
    batch_status = batch.get("status")
    print(batch_status)  # validating / in_progress / completed / failed ...
    if batch_status in {"completed", "failed", "expired", "cancelled"}:
        break
    time.sleep(5)

# 4) 完成后下载结果(失败行在 error_file_id 中,可单独重试)
if batch_status == "completed":
    download_batch_file(batch_id, "output", Path("output.jsonl"))
    if batch.get("error_file_id"):
        download_batch_file(batch_id, "error", Path("error.jsonl"))

任务完成后结果文件仍带回原来的 custom_id,业务可以把摘要准确写回对应文章。部分数据失败时,只需下载错误文件并重试失败行,已完成的结果直接保留。

降本之后,在线调用还要稳

成本只是第一关。AI Function 进入生产环境后,网络抖动、账号限流、API Key 异常和缓存故障,都可能影响一次写入或查询。阿里云 Milvus 在性能与稳定性上做了几项强化:

  • 内网直连:AI Function 服务与百炼通过内网打通,调用路径更短,减少公网调用带来的性能损耗与网络波动。

  • 多账号负载均衡与故障切换:支持多账号分摊调用、异常 API Key 自动切换,降低单账号限流或单 Key 异常对业务连续性的影响。

  • 缓存延迟降级:Redis 短暂不可用或读取超时时,系统绕过缓存继续调用模型;开启延迟降级后,连续慢读或错误达到设定次数,缓存默认进入 60 秒降级窗口,触发降级的结果会被丢弃、请求直接回源模型,窗口结束后系统自动探测缓存是否恢复。

这条策略很明确:宁可短时间多调用模型,也要避免缓存拖慢请求或把异常结果带进主链路。Batch 对局部失败同样保留了单独处理的空间------成功结果与错误结果分开下载,只重试失败部分,已完成的数据无需整批重做。

内容进入模型前后,都可以多一道检查

客服对话、工单处理和自动内容发布还会遇到另一类问题:用户输入可能含风险内容,模型输出也可能不适合直接交付。Data Inspection 可以检查模型调用前的输入、返回前的输出,或两端同时开启:

  • 输入检查适合客服、工单和用户提问,在风险内容进入模型前完成判断;

  • 输出检查适合摘要、营销文案和自动答复,在内容进入发布流程前再检查一次;

  • 双向检查适合风险要求更高、结果会直接触达用户的业务。

命中安全策略的内容会被拦截或返回安全拒答,正常内容继续走原有流程。Data Inspection 负责的是 AI Function 文本处理过程中的内容安全;账号权限、网络隔离和数据加密仍需按企业安全规范分别配置。

实战效果

把降本与稳定两条路径放进同一套业务,各类数据各走合适的路径,在线体验和成本不用互相让步。两种降本能力面向的是两种浪费:

业务特征 Embedding Cache AI Batch
数据特点 相同文本反复出现 首次处理的大批量数据
返回要求 在线返回 允许延后完成
节省来源 减少重复模型调用 按实时推理价格 50% 计费
常见场景 FAQ、热门商品、重复导入 历史加工、夜间任务、模型评测

落到电商场景:新商品上架继续实时向量化,热门商品重复同步由 Embedding Cache 复用向量,几百万条历史商品的摘要和标签交给 Batch;在线调用通过内网直连、多账号负载均衡与异常 Key 切换保持连续;涉及内容生成的流程再按风险等级开启输入 / 输出 / 双向检查。与传统做法相比,收益很直观:

维度 优化前 接入阿里云 Milvus AI Function 后
重复内容向量化 每次都调用模型 Embedding Cache 命中即复用,重复内容 0 Token
离线大批量任务 按实时价格、挤占在线额度 Batch 离线执行,50% 计费、错峰运行
突发流量 / 单账号限流 单账号打满即失败 多账号负载均衡 + 异常 Key 故障切换
缓存故障 可能拖垮主链路 自动降级回源,主链路不中断
内容安全 事后人工、被动处置 Data Inspection 前置拦截,发不出去

总结

每项能力都有清晰的职责:Cache 减少重复计算,Batch 降低离线任务单价,稳定性机制守住在线调用,Data Inspection 控制内容风险。真正用于生产的 AI Function,很少只依赖某一个开关------先按时效拆数据(在线走实时、存量与周期任务走 Batch),再按重复率让 Embedding Cache 接住反复出现的内容,在线调用靠内网直连与多账号切换保持连续,最后按风险等级挂上内容检查。成本、稳定和安全,也就从口号落到了每一次请求里。

让 AI 落地,不必再从复杂链路开始

阿里云 Milvus 是目前唯一支持 AI Native 的 Milvus 云服务。它不仅提供高性能、全托管的向量检索能力,更将 AI Function 与 AI-Gateway 原生嵌入数据链路,让模型能力真正成为数据库的一部分。

从数据进入阿里云 Milvus 的那一刻起,理解、加工、检索和生成便可以在一条原生链路中完成。阿里云 Milvus 正在把「接入 AI」变成「原生拥有 AI」。

相关推荐
问商十三载4 小时前
GEO优化有用吗?基于检索与信源机制的有效性分析
大数据·人工智能·机器学习
m4Rk_10 小时前
【论文阅读】Agent 记忆机制(83):Inside Out——用可演化 PersonaTree 构建 Agent 的核心长期记忆
论文阅读·人工智能·学习·开源·github
运行时异常10 小时前
【WMS 仓储系统集成 AI Agent 实战】第 8 讲(终篇):生产部署与并发安全——Semaphore 放进 Flux.defer 的坑,压测抓了一晚上
人工智能·安全
杨杨杨大侠10 小时前
Jev、Kev、Laya:决策模型怎么选,什么时候需要微调?
人工智能·python·agent
李福春10 小时前
markdown表格标题渲染判定
agent·架构师同盟·腾讯云架构师同盟
代码方舟10 小时前
零信任架构实战:基于天远人企关联构建自动化供应链金融网关
运维·人工智能·架构·自动化
虹科网络安全11 小时前
“顶会”看安全(十八):超越越狱:揭示由能力边界模糊引发的 LLM 应用安全风险
人工智能·安全
Maiko Star11 小时前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
鬓戈11 小时前
Rust 语言与 AI 应用生态调研及学习路径
人工智能·学习·rust
天远API11 小时前
零信任架构实战:基于天远人企关联构建自动化图谱网关
网络·人工智能·架构·自动化