跳到主要内容
版本:Dev

语义路由

语义路由器为应用提供一个稳定的模型别名,同时由 AISIX 根据每个请求的语义选择目标。AISIX 对请求中最新的用户文本进行向量嵌入,将它与各路由的示例进行比较,再将请求分发到最佳匹配目标。如果没有路由达到阈值,AISIX 会使用默认目标。每个目标可以是直接模型,也可以是模型组。

当请求需要按主题到达不同模型,而应用不应自行选择模型时,请使用语义路由。例如,同一个别名可以把法律问题发送到推理模型,把翻译请求发送到多语言模型,并把所有其他请求发送到通用模型。

语义路由器支持以下端点:

  • /v1/chat/completions
  • /v1/messages
  • /v1/messages/count_tokens
  • /v1/responses

因此 Claude Code、Codex 等编码智能体可以直接调用语义路由器。要让一个智能体会话保持在其首个请求命中的路由上,请开启会话固定。

本页介绍基于向量嵌入的决策后端。语义路由器也可以改由 TypeSafe 托管的 jev 决策模型根据各路由的描述选择路由,无需示例和阈值。参见使用 TypeSafe Jev 进行语义路由。

语义路由的工作原理​

对于每个请求,AISIX 使用配置的向量嵌入模型,将最新的用户文本与路由示例进行比较,再把请求分发到一个目标。

AISIX 会在第一个需要路由示例向量的请求到达时对其进行向量嵌入,并在网关进程中缓存这些向量。当示例、向量嵌入模型或向量维度发生变化时,AISIX 会重新计算向量。因此,每个请求只需要为其最新的用户文本调用一次向量嵌入端点,之后在本地完成相似度计算。

准备工作​

开始前,请准备以下资源:

  • 一个用于 OpenAI 兼容 /v1/embeddings 端点的服务提供方密钥,该端点需要返回浮点数向量。
  • 一个默认目标,以及至少一个路由目标。每个目标可以是直接模型或模型组。路由器会向它们发送 Chat Completions 请求,因此直接模型的服务提供方必须提供 Chat Completions。AISIX Enterprise 和 AISIX Cloud 均不允许不提供 Chat Completions 的模型(例如使用 jina、runwayml 或 typesafe 服务提供方密钥的模型)作为路由目标、默认目标或失败目标,也不允许把被引用的模型换到这类密钥上。
  • 一个可以调用语义路由器别名的调用方 API Key。
  • 对于 AISIX Enterprise 或 AISIX Cloud,需要一个已接入网关的环境,以及管理模型和调用方 API Key 的权限。
  • 对于开源 AISIX 网关,需要有权访问声明式资源文件和网关进程。

配置语义路由器​

请先创建向量嵌入模型、默认模型和路由目标,再创建语义路由器。控制面通过模型 ID 引用这些资源,开源 AISIX 网关则在声明式资源文件中通过 display_name 引用它们。

向量嵌入模型的 dimensions 必须与上游端点返回的数值数量一致。只有当 AISIX 必须在比较向量前对其进行归一化时,才将 normalize 设置为 false。

每条路由都需要名称、一个目标和至少一个示例。描述为可选字段。路由级 threshold 会覆盖该路由所使用的路由器级阈值。

AISIX Enterprise 或 AISIX Cloud​

导出控制面连接信息和向量嵌入模型使用的服务提供方密钥:

# AISIX_CP 是 Admin API 基础 URL;应包含 /api,且末尾不要带斜杠。
# 本地 AISIX Enterprise 快速入门使用 http://localhost:8080/api。
export AISIX_CP="YOUR_AISIX_CONTROL_PLANE_API_URL"
export AISIX_TOKEN="YOUR_ADMIN_TOKEN"
export ENV_ID="YOUR_ENVIRONMENT_ID"
export PROVIDER_KEY_ID="YOUR_EMBEDDING_PROVIDER_KEY_ID"

创建向量嵌入模型并记录其 ID:

EMBEDDING_MODEL_ID=$(curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/models" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "embedding",
"display_name": "embedding-prod",
"model_name": "text-embedding-3-small",
"provider_key_id": "'"$PROVIDER_KEY_ID"'",
"embedding": {
"dimensions": 1536,
"normalize": true
}
}' | jq -r '.model.id')

导出目标模型的 ID。每个目标可以是直接模型,也可以是模型组:

export DEFAULT_MODEL_ID="YOUR_DEFAULT_MODEL_ID"
export LEGAL_MODEL_ID="YOUR_LEGAL_MODEL_ID"
export TRANSLATION_MODEL_ID="YOUR_TRANSLATION_MODEL_ID"

创建语义路由器。其默认阈值会应用于所有未自行定义阈值的路由:

SEMANTIC_MODEL_ID=$(curl -sS -X POST "$AISIX_CP/environments/$ENV_ID/models" \
-H "Authorization: Bearer $AISIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"kind": "semantic",
"display_name": "topic-router",
"semantic": {
"embedding_model_id": "'"$EMBEDDING_MODEL_ID"'",
"default_model_id": "'"$DEFAULT_MODEL_ID"'",
"threshold": 0.75,
"routes": [
{
"name": "legal",
"target_model_id": "'"$LEGAL_MODEL_ID"'",
"description": "Contract and legal-risk analysis",
"examples": [
"Analyze this contract for legal risk",
"Review this NDA for liability exposure"
],
"threshold": 0.8
},
{
"name": "translation",
"target_model_id": "'"$TRANSLATION_MODEL_ID"'",
"examples": [
"Translate this paragraph to French"
]
}
]
}
}' | jq -r '.model.id')

把 SEMANTIC_MODEL_ID 添加到调用方 API Key 的 allowed_models 列表。保存后的配置会自动投射到已接入的网关。

你也可以在控制台的 模型 页面创建和编辑向量嵌入模型与语义路由器。语义路由器表单提供两个阈值调优工具:

  • 测试路由 会展示为提示词选择的路由、每条路由的相似度分数,以及该分数是否达到阈值。
  • 自动检测阈值 会根据已配置示例集内部及相互之间的相似度,为每条路由推荐初始阈值。

这两个工具都从控制面调用向量嵌入端点,因此控制面服务必须能够访问该端点。

开源 AISIX 网关​

将向量嵌入模型和语义路由器添加到 models 集合。semantic 块中的引用使用模型的 display_name:

resources.yaml(语义路由模型)
models:
- display_name: embedding-prod
provider: openai
model_name: text-embedding-3-small
provider_key: embeddings-provider
embedding:
dimensions: 1536
normalize: true

- display_name: topic-router
semantic:
embedding_model: embedding-prod
default: general-chat
match:
distance_metric: cosine
aggregation: max
threshold: 0.75
routes:
- name: legal
target: legal-chat
description: Contract and legal-risk analysis
examples:
- Analyze this contract for legal risk
- Review this NDA for liability exposure
threshold: 0.8
- name: translation
target: translation-chat
examples:
- Translate this paragraph to French

完整资源文件还必须包含被引用的服务提供方密钥,以及 general-chat、legal-chat 和 translation-chat 直接模型。将 topic-router 添加到调用方密钥的 allowed_models,然后验证并重新加载该文件。

验证路由选择​

导出所配置部署的网关 URL 和调用方 API Key:

# 使用网关源地址,不要附加末尾斜杠或端点路径。
# 本地快速入门使用 http://127.0.0.1:3000。
export AISIX_PROXY="YOUR_AISIX_GATEWAY_URL"
export AISIX_API_KEY="YOUR_CALLER_API_KEY"

发送一个应匹配法律路由的请求:

curl -sSi -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $AISIX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "topic-router",
"messages": [
{"role": "user", "content": "Review this contract for liability risk."}
]
}'

响应体仍以 topic-router 作为面向调用方的模型名称。要查看匹配了哪条路由,请按响应头 x-aisix-request-id 找到该请求的访问日志行:匹配成功的请求会带有 semantic_route="legal"。如果网关启动配置将 proxy.expose_debug_headers 设为 true,响应还会包含 x-aisix-route: legal,以及带有实际处理请求的直接模型名称的 x-aisix-served-by。

发送一个与所有路由均无关的提示词,确认 AISIX 使用默认模型。该请求的访问日志行带有 semantic_fallback 而不是 semantic_route,响应也不包含 x-aisix-route。

请求的访问日志行会记录路由器的决策:匹配的路由、其相似度分数,以及没有路由处理请求时由默认模型处理的原因。参见读取语义路由决策。

调整匹配行为​

AISIX 选择路由时会应用以下规则:

  • 只对一个用户轮进行分类,选取方式参见参与分类的文本。
  • 每条路由的分数是请求与该路由示例之间的最高余弦相似度。
  • 如果分数不低于路由级阈值,或在路由未覆盖时不低于路由器级阈值,该路由即为匹配。
  • 如果有多条路由匹配,分数最高的路由胜出。如果没有路由匹配,AISIX 使用默认模型。

参与分类的文本​

语义路由器支持的所有端点都采用同一规则。AISIX 从最新到最早依次检查请求中的用户轮,对第一个带有用户亲自输入的文本的用户轮进行分类:

  • 只读取该轮的文本部分。如果有多个文本部分,AISIX 会用空格把它们连接起来。
  • 文本中的每个 <system-reminder>…</system-reminder> 块都会被移除,块前后的文本保留。
  • 对于 Codex 客户端(按 User-Agent 识别,规则见会话 ID 的来源),AISIX 还会移除 Codex 注入用户轮的块:<environment_context>、<recommended_plugins>、<user_instructions> 和 <environments_instructions>,以及从 # AGENTS.md instructions for … 到 </instructions> 的指令块。其他客户端的文本只移除 <system-reminder> 块。
  • 只包含工具结果、图片或 reminder 的用户轮没有这样的文本,AISIX 会继续检查更早的一轮。
  • 系统提示词、Responses API 的 instructions 和助手轮永远不参与分类。

没有这样的文本的请求会直接发送到默认目标,不调用向量嵌入端点,除非该请求的会话已固定到某条路由。

因此,Claude Code、Codex 等智能体在工具调用循环中发出的每个请求,仍按用户最近一次真实提问分类,而不是按工具结果分类。各协议中的用户轮分别是:/v1/chat/completions 上的 user 消息;/v1/messages 和 /v1/messages/count_tokens 上内容为字符串或 text 块的 user 消息;/v1/responses 上的字符串 input,或 user 消息项中的 input_text 部分。

在 /v1/chat/completions 上,如果最后一条用户消息只有图片,AISIX 会对更早的一条带文本的用户消息进行分类。1.10.0 之前的网关会把这类请求直接发送到默认目标。

跨语言匹配取决于向量嵌入模型。请使用来自预期工作负载的代表性提示词和示例来调整阈值,不要假定一个阈值适用于所有向量嵌入模型。对抗性提示词也可能影响路由决策,因此,如果目标选择会产生安全或合规后果,请在分发前应用输入安全护栏。请把这些安全护栏附加到语义路由模型本身:附加到目标模型上的安全护栏,不会在调用方寻址语义路由模型的请求上运行。参见安全护栏作用范围由调用方寻址的模型决定。

处理向量嵌入失败​

使用 embedding_timeout_ms 限制向量嵌入调用时间,并通过 on_embedding_failure 选择在发生向量嵌入错误或超时后的行为。默认行为是使用路由器的默认模型。

在 AISIX Enterprise 和 AISIX Cloud 中,回退策略是一个对象。要以 503 拒绝请求,请配置:

{
"semantic": {
"embedding_timeout_ms": 500,
"on_embedding_failure": {
"mode": "fail"
}
}
}

将 mode 设置为 default 可使用默认模型。要使用另一个直接模型或模型组,请把 mode 设置为 target 并提供其 target_model_id。

使用 fail 时,返回的 503 携带 error.code semantic_routing_failed,消息中给出路由器名称和原因,例如 embedding_timeout。访问日志行记录 semantic_fallback="decision_failed" 和 error_class="semantic_decision_failed"。各原因的含义参见语义路由失败。

在开源资源文件中,请使用 default、fail,或一个命名直接模型或模型组的对象:

semantic:
embedding_timeout_ms: 500
on_embedding_failure:
target: safe-chat

路由到模型组​

路由目标、默认目标和失败目标都可以是模型组(kind: routing,即多目标模型),而不只是直接模型。当一条路由需要把流量分散到多个部署上,或在它们之间故障转移,而不是只到达单个模型时,请使用模型组。

在 AISIX Enterprise 和 AISIX Cloud 中,在原本填写直接模型 ID 的位置填写模型组的模型 ID 即可:路由的 target_model_id、default_model_id,或 on_embedding_failure.target_model_id。在控制台中,目标下拉列表会给每个模型组加上「模型组」标记。合议模型、另一个语义路由器或向量嵌入模型不能作为目标,保存时返回 400。模型组还必须至少有一个成员所用的服务提供方支持 Chat Completions,否则同样被拒绝。该检查只在保存路由器时进行:之后对模型组或其成员的修改不会再次检查。

在开源资源文件中,像引用直接模型一样,在 target、default 或 on_embedding_failure.target 中写模型组的名称。

路由器选中模型组后,模型组按调用方直接寻址它时的方式分发请求:由它的策略选择成员,它的故障转移、冷却和成员限流照常生效。如果模型组没有可分发的成员,或它自身的客户端 IP 白名单排除了调用方,请求由路由器的默认目标处理,访问日志记录 semantic_fallback="target_unavailable"。

各机制分别作用于以下层级:

机制作用对象
安全护栏、模型访问控制和响应缓存调用方寻址的语义路由器。
限流路由器自身的限流每个请求计一次;模型组自身的限流在每个选中该组的请求中计一次;成员的限流在每次发往它的上游尝试中计一次。参见 API Key 与模型限流。
成本与用量实际处理请求的成员。
指标model 标签是语义路由器,upstream_model 是成员。

会话固定把会话固定到路由,也就是固定到模型组,而不会固定到组内的某个成员。如果还需要让同一会话始终落到同一个成员,请给模型组使用 consistent_hash 策略并按会话请求头做哈希,做法参见会话绑定与池间故障转移。例如,一个同时服务 Claude Code 和其他客户端的模型组可以按以下顺序读取哈希键,取第一个非空值:

"routing": {
"strategy": "consistent_hash",
"hash_on": [
{"type": "header", "name": "x-claude-code-session-id"},
{"type": "header", "name": "x-aisix-session-id"},
{"type": "api_key"}
],
"targets": [...]
}

1.10.0 之前的网关能加载以模型组为目标的路由器,但发往该模型组的每个请求都会失败。因此,只要环境中注册了低于 1.10.0 的网关,AISIX Enterprise 和 AISIX Cloud 就会以 422 DP_INCOMPATIBLE 拒绝保存这样的路由器。

会话固定​

默认情况下,语义路由器对每个请求单独分类,因此同一个智能体对话可能在不同轮次之间切换模型。会话固定让会话保持在其首个命中请求所走的路由上:该会话之后的请求跳过分类,直接发送到该路由的目标。向量嵌入后端和 TypeSafe jev 后端都支持会话固定。

开启会话固定​

在路由器的 semantic 配置中添加 session_affinity 块:

字段默认值说明
enabledfalse是否固定会话。为 false 时,已有的固定不会被使用。
ttl_seconds3600固定在最后一次被使用后保持多久,取值 60 到 604800 秒。

不配置该块时,每个请求都会分类。

在 AISIX Enterprise 和 AISIX Cloud 中:

{
"semantic": {
"session_affinity": {
"enabled": true,
"ttl_seconds": 3600
}
}
}

在控制台中,在语义路由器表单里设置 会话固定 和 固定有效期(秒)。

在开源资源文件中:

semantic:
session_affinity:
enabled: true
ttl_seconds: 3600

固定的行为​

  • 会话中第一个命中路由的请求,在路由被选中时就把会话固定到该路由。该会话之后的请求不再分类,直接发送到固定的路由,每个这样的请求都会把固定再延长 ttl_seconds。
  • 固定属于语义路由器、调用方 API Key 和会话 ID 三者的组合。两个 API Key 发送相同的会话 ID,或同一会话调用两个路由器,都不会共用固定。
  • 只有命中路由的请求才会创建固定。发送到默认目标或失败目标的请求不会创建固定,该会话的下一个请求照常分类。
  • 固定路由的目标暂时无法处理该请求时,由默认目标处理,固定保留。
  • 固定的目标已不再是该路由器任何路由的目标时(例如路由器配置被修改),固定作废,请求重新分类。
  • /v1/messages/count_tokens 会使用并延长已有的固定,但不会创建新的固定。
  • 编码智能体在会话主循环之外发出的旁路请求(例如生成会话标题)同样不会创建固定。会话已有固定时,旁路请求照常走固定并延长它;没有固定时照常分类。AISIX 通过 x-claude-code-request-class: auxiliary 请求头,或请求文本与 Claude Code、Codex 已知的标题生成、进度摘要、输入建议提示语相符,来识别旁路请求。

按提示语识别旁路请求只对 AISIX 已知的提示语有效,客户端版本更新改了提示语后,旁路请求就可能固定其会话。要让这类请求完全不参与分类,请把客户端的后台模型或小模型指向直接模型,而不是语义路由器。对于 Claude Code,即 ANTHROPIC_DEFAULT_HAIKU_MODEL 设置的小型快速模型,参见配置 Claude Code。

在 AISIX Enterprise 和 AISIX Cloud 中,同一环境的所有网关通过控制面共享该环境的固定,因此无论请求到达哪个网关,会话都保持在同一路由上。每个环境大约可保存 1,000,000 条固定;超出后,新的固定只保存在创建它的网关上。网关无法访问共享的固定时,会改用自己本地的固定,请求不受影响;在恢复访问之前,不同网关可能把同一会话固定到不同路由。开源网关把固定保存在自己的进程中。

会话 ID 的来源​

AISIX 按以下顺序读取会话 ID,取第一个有效值。超过 256 字节的值会被忽略。

  1. x-aisix-session-id 请求头。
  2. 任意 x-<vendor>-session-id 请求头,例如 Claude Code 发送的 x-claude-code-session-id。同时存在多个时,取请求头名称按字典序最小的一个。
  3. 对于 Codex 客户端(User-Agent 以 codex 开头,后接 -、_、/ 或空格):session-id、session_id、thread-id 或 conversation_id 请求头。
  4. x-session-id 请求头。
  5. 请求体的 metadata.user_id:对象的 session_id;内容为 JSON 对象的字符串中的 session_id;或字符串中最后一个 _session_ 之后的部分。
  6. 请求体的 metadata.session_id。
  7. W3C baggage 请求头中的 session.id 条目。

第 2、3、4 项的请求头只有在值至少为 8 个字母、数字、_ 或 - 时才有效。不带会话 ID 的请求照常分类,不会创建固定。

Claude Code 和 Codex 默认就会发送上述列表中的会话 ID,因此无需在客户端额外配置即可固定会话。对于其他客户端,请发送 x-aisix-session-id,并在整个对话中保持其值不变:

curl -sS -X POST "$AISIX_PROXY/v1/chat/completions" \
-H "Authorization: Bearer $AISIX_API_KEY" \
-H "x-aisix-session-id: conversation-1a2b3c" \
-H "Content-Type: application/json" \
-d '{
"model": "topic-router",
"messages": [
{"role": "user", "content": "Review this contract for liability risk."}
]
}'

观察被固定的请求​

发送到固定路由的请求,其访问日志行带有 semantic_pinned=true,并在 semantic_route 中记录该路由。由于没有进行分类,该行没有 semantic_score。如果固定的目标不可用,该行带有 semantic_pinned=true 和 semantic_fallback="target_unavailable",没有 semantic_route。

网关版本​

会话固定需要 1.10.0 或更高版本的网关。更早的网关会忽略 session_affinity 块,对每个请求都进行分类。在存在这类网关的环境中,AISIX Enterprise 和 AISIX Cloud 仍会保存路由器,并提示这些网关会忽略该字段。

下一步​

如果希望按描述而不是按示例相似度选择路由,请使用使用 TypeSafe Jev 进行语义路由。要创建路由可以指向的模型组,请参见多目标路由与故障转移。如果一个请求需要调用多个合议成员并合成它们的回答,请继续阅读合议模型。如果请求在模型分发前需要策略检查,请使用安全护栏。