练一个条件反射
条件反射可以用你自己的数据"练"出来:提供一批"输入 → 正确答案"的样本,象信在专用 GPU 上从基础条件反射模型出发,练出一个只属于你组织的反射,之后用 model: "xiangxin-reflex:<名字>" 调用,请求和响应格式与象信一号完全相同。
练反射目前免费(以后会单独计费,届时会提前公告);练好的反射按条件反射的价格计费,输入 ¥0.00042 / 百万 token,见定价与额度。
整个流程五步:
- 定义问题(和平时调用象信一样)
- 准备样本:一行一个
{state, answers} - 提交:
POST /v1/reflexes - 等它练好,读练前 / 练后指标
- 用
xiangxin-reflex:<名字>调用;数据变了就重练
下面用一个完整的例子走一遍:把电商客服系统里一串用来分流工单的正则,换成一个条件反射。
起点:一串越写越长的正则
很多客服系统的工单分流是这样写的:
import re
ROUTES = [
("退换货", re.compile(r"退货|退款|换货|退钱|七天无理由|不想要了")),
("物流", re.compile(r"快递|物流|发货|没收到|还没到|派送|签收")),
("发票", re.compile(r"发票|开票|抬头|税号")),
("账户", re.compile(r"登录|登不上|密码|验证码|绑定手机")),
]
def route(text: str) -> str:
for team, pattern in ROUTES:
if pattern.search(text):
return team
return "人工"它能处理最常见的说法,但问题也很典型:
| 工单 | 正则的结果 | 应该是 | 原因 |
|---|---|---|---|
| 我不是要退款,就想问问快递到哪了 | 退换货 | 物流 | 只认字面,不懂否定;规则顺序决定结果 |
| 东西到现在影子都没见着 | 人工 | 物流 | 没人想到这种说法 |
| 《密码学》发错版本了,要换一本 | 账户 | 退换货 | "密码"碰巧命中了账户规则 |
每修一个错例就要加一条规则、调一次顺序,规则之间开始互相打架;规则越多,每条工单要匹配的模式也越多。
而另一方面,这个客服系统里其实已经有答案:每张历史工单最后由哪个组处理,都记录在工单系统里。这就是现成的标注数据。
第一步:定义问题
问题的写法与调用象信一号时完全相同(见原语)。一次最多 32 个问题,Choice 最多 255 个选项,Score 2–10 档。这里问两个:
{
"team": {
"type": "choice",
"instructions": "这张客服工单应该转给哪个组处理?",
"criteria": {
"退换货": "退货、换货、退款、商品质量问题",
"物流": "发货进度、快递查询、未收到货、派送问题",
"发票": "开票、发票抬头、税号、专票普票",
"账户": "登录、密码、验证码、手机号绑定",
"其他": "以上都不是"
}
},
"wants_refund": {
"type": "noul",
"instructions": "用户是否明确要求退款?"
}
}问题会和反射绑定
反射是针对这组问题练的。之后调用时,请使用与训练时完全相同的 instructions 和 criteria(包括选项键名)。要改问题,就改完之后重练。
第二步:准备样本
样本是一个 JSONL 文件,每行一个 JSON 对象:
{"state": "买的电饭煲内胆有划痕,想换一个新的", "answers": {"team": "退换货", "wants_refund": false}}
{"state": "我不是要退款,就想问问快递到哪了,三天没动静", "answers": {"team": "物流", "wants_refund": false}}
{"state": "东西到现在影子都没见着,不要了,钱退我", "answers": {"team": "退换货", "wants_refund": true}}
{"state": "公司报销要专票,抬头能改吗", "answers": {"team": "发票"}}
{"state": "换了手机号收不到验证码,登不上去", "answers": {"team": "账户"}}state:与推理时的state一样,可以是字符串、JSON 对象或数组。训练时用什么形式,调用时就用什么形式——如果线上请求的state是{"工单": …, "订单状态": …},样本也要这样组织。answers:以问题 ID 为键的标注。可以只标其中一部分问题(上面第 4、5 行只标了team),没标的问题不参与这一行的训练。
标注的写法按原语类型:
| 原语 | 标注 | 例子 |
|---|---|---|
| Noul | true 或 false(也接受 1 / 0) | "wants_refund": true |
| Choice | criteria 里的一个选项键 | "team": "物流" |
| Score | 档位下标,从 0 开始的整数 | "anger": 2 表示 criteria 里的第三档 |
标注写错(例如 Choice 的值不在选项里、Score 下标越界)时,提交会返回 422,detail 里写明是第几条样本、哪个问题出了错。
在这个例子里,样本可以直接从工单系统导出:state 是用户的原始留言,team 是这张工单最终的处理组,wants_refund 看是否走了退款流程。
要多少条
| 最少 | 10 条(少于 10 条返回 422 too_few_examples) |
| 最多 | 50,000 条,请求体不超过 50MB |
| 建议 | 每个问题 200 条以上;每个选项、每个档位都要有足够多的例子,Noul 的真和假都要有 |
| 留出验证 | 样本 ≥ 20 条时,随机留出 20%(最多 1,000 条)做验证集,用来决定何时停止训练、校准概率,并计算练后指标;少于 20 条时不留出,指标按训练集计算,只能作参考 |
几条经验:
- 数据要像线上流量。 样本最好直接取自真实历史数据,而不是人工编写的"标准说法"。正则漏掉的那些奇怪写法,恰恰是反射最需要学的。
- 标注要一致。 同一种工单有时标"物流"、有时标"退换货",反射就会学得犹豫。先统一标注口径,比多加数据更有效。
- 稀有类别要够。 某个选项只有两三条样本,反射几乎学不会它。可以有意多采这些样本,或者把它并入"其他"。
- 没有现成标注? 可以先用系统一(
xiangxin-s1)给一批历史数据作答,人工抽查、改正拿不准的样本,再拿去练。
长度限制
条件反射逐个问题读取"问题 + state",每一行最多 2,048 token。训练时超长的样本会被截断(保留开头和结尾);推理时超长会返回 422 max_tokens_exceeded,不会截断。请让 state 只包含判断需要的内容。
第三步:提交
把问题和样本一起发到 POST /v1/reflexes。反射的名字只能用小写字母、数字和连字符(^[a-z0-9][a-z0-9-]{0,62}$),它会成为模型名 xiangxin-reflex:<名字> 的一部分。
# 用 jq 把 questions.json 和 examples.jsonl 拼成请求体
jq -n --slurpfile q questions.json --slurpfile ex examples.jsonl \
'{name: "ticket-router", description: "电商客服工单分流", questions: $q[0], examples: $ex}' |
curl -X POST https://api.xiangxinai.cn/v1/reflexes \
-H "Authorization: Bearer $XIANGXIN_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @-import json
from xiangxin import XiangxinClient
with open("questions.json", encoding="utf-8") as f:
questions = json.load(f)
with open("examples.jsonl", encoding="utf-8") as f:
examples = [json.loads(line) for line in f if line.strip()]
with XiangxinClient() as client:
reflex = client.reflexes.create(
"ticket-router",
questions=questions,
examples=examples,
description="电商客服工单分流",
)
print(reflex.status, reflex.model) # queued xiangxin-reflex:ticket-routerimport { readFileSync } from 'node:fs'
import { XiangxinClient } from '@xiangxinai/sdk'
const questions = JSON.parse(readFileSync('questions.json', 'utf8'))
const examples = readFileSync('examples.jsonl', 'utf8')
.split('\n')
.filter((line) => line.trim())
.map((line) => JSON.parse(line))
const client = new XiangxinClient()
const reflex = await client.reflexes.create({
name: 'ticket-router',
description: '电商客服工单分流',
questions,
examples,
})
console.log(reflex.status, reflex.model) // queued xiangxin-reflex:ticket-router提交成功后立即返回反射对象,状态为 queued:
{
"id": "rf_7m2kq9x4vbd3",
"name": "ticket-router",
"model": "xiangxin-reflex:ticket-router",
"description": "电商客服工单分流",
"status": "queued",
"usable": false,
"progress": 0.0,
"stage": "queued",
"queue_position": 0,
"questions": { "team": { "type": "choice", "...": "..." }, "wants_refund": { "type": "noul", "...": "..." } },
"examples": 1200,
"metrics": null,
"error": null,
"created_at": "2026-09-25T08:30:00Z",
"updated_at": "2026-09-25T08:30:00Z",
"trained_at": null
}第四步:等它练好,读指标
训练在专用 GPU 上排队进行,所有组织共用一个队列。用 GET /v1/reflexes/{名字} 查询进度,每隔几秒查一次即可;SDK 的 reflexes.wait 会替你轮询:
curl https://api.xiangxinai.cn/v1/reflexes/ticket-router \
-H "Authorization: Bearer $XIANGXIN_API_KEY"with XiangxinClient() as client:
# 每 2 秒查询一次,直到 ready / failed / cancelled;失败或取消时照常返回,不抛异常
reflex = client.reflexes.wait("ticket-router")
print(reflex.status, reflex.error)
if reflex.metrics:
print(reflex.metrics.before.accuracy, "→", reflex.metrics.after.accuracy)const reflex = await client.reflexes.wait('ticket-router')
console.log(reflex.status, reflex.error)
console.log(reflex.metrics?.before?.accuracy, '→', reflex.metrics?.after?.accuracy)status 的变化:
status | 含义 |
|---|---|
queued | 排队中,queue_position 是前面还有几个任务 |
training | 训练中,progress 为 0–1 的进度,stage 为当前阶段 |
ready | 练好了,可以调用 |
failed | 失败,原因见 error;改好数据后重新提交即可 |
cancelled | 已取消 |
练好之后,metrics 里是练前和练后在同一份验证集上的成绩(以下为示意数值):
{
"examples": 1200,
"train_examples": 960,
"val_examples": 240,
"train_rows": 1920,
"val_rows": 480,
"evaluated_on": "val",
"epochs": 6.0,
"steps": 180,
"temperature": 1.12,
"duration_s": 95.2,
"before": {"accuracy": 0.61, "log_loss": 0.93, "ece": 0.12,
"per_question": {"team": {"accuracy": 0.58, "n": 240}, "wants_refund": {"accuracy": 0.65, "n": 240}}},
"after": {"accuracy": 0.97, "log_loss": 0.09, "ece": 0.02,
"per_question": {"team": {"accuracy": 0.96, "n": 240}, "wants_refund": {"accuracy": 0.98, "n": 240}}}
}before:未经训练的基础条件反射xiangxin-reflex在验证集上的成绩;after:练好的反射的成绩。两者的差距就是"练"带来的提升。accuracy:最可能的答案与标注一致的比例,按"样本 × 已标注的问题"逐个统计。per_question按问题分别统计,n是该问题参与验证的样本数。log_loss:对数损失,越低越好。它同时惩罚"答错"和"答对但没把握"。ece:期望校准误差,越低说明概率越可信("说 0.9 的,大约九成是对的")。见置信度。evaluated_on:val表示按留出的验证集计算;train表示样本少于 20 条、没有留出验证集,指标是在训练数据上算的,会偏乐观。- 其余字段:
train_rows/val_rows是展开成"样本 × 问题"之后的行数,epochs/steps是训练轮数与步数,temperature是用验证集校准概率时得到的温度,duration_s是训练耗时(秒)。
训练时会在验证集上挑选成绩最好的一版;如果练了反而不如基础模型,就保留基础模型的效果,不会让反射变得更差。
怎么读:
after明显好于before,且达到你的要求:可以上线。建议再用验证集之外的一批新数据抽查,并按置信度门控路由的方法定阈值。- 某个问题的
per_question明显偏低:检查这个问题的标注口径是否一致、各选项的样本是否够多、选项之间是否重叠。 before已经很高:基础条件反射对这个问题已经足够好,直接用xiangxin-reflex即可,不一定要练。after始终上不去:这个判断可能需要世界知识或推理,条件反射学不会,改用系统一,或者用"反射 + 系统一"的级联。
第五步:调用
练好的反射通过同一个评估接口 POST /v1/systemone 调用,model 写 xiangxin-reflex:<名字>,questions 与训练时相同:
curl -X POST https://api.xiangxinai.cn/v1/systemone \
-H "Authorization: Bearer $XIANGXIN_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --slurpfile q questions.json \
'{model: "xiangxin-reflex:ticket-router", state: "我不是要退款,就想问问快递到哪了", questions: $q[0]}')"import json
from xiangxin import XiangxinClient, reflex_model
with open("questions.json", encoding="utf-8") as f:
QUESTIONS = json.load(f)
with XiangxinClient() as client:
resp = client.system_one(
state="我不是要退款,就想问问快递到哪了",
questions=QUESTIONS,
model=reflex_model("ticket-router"), # 即 "xiangxin-reflex:ticket-router"
)
team = resp.answers["team"]
print(resp.model, team.choice, team.confidence, resp.answers["wants_refund"].noul)import { reflexModel } from '@xiangxinai/sdk'
const resp = await client.systemOne({
state: '我不是要退款,就想问问快递到哪了',
questions,
model: reflexModel('ticket-router'), // 即 'xiangxin-reflex:ticket-router'
})
console.log(resp.model, resp.answers.team.choice, resp.answers.team.confidence)响应与象信一号完全相同,model 字段返回 xiangxin-reflex:ticket-router,响应头 x-xiangxin-model-ms 保留一位小数。
- 反射第一次训练还没完成时调用,返回
409 {"detail": "reflex_not_ready"}(SDK 抛出ConflictError)。 - 名字不存在或已删除,返回
404 {"detail": "model_not_found"}。 - 反射只在你的组织内可见,其他组织无法调用。
- 在控制台 Playground 的模型下拉框里也能选到本组织练好的反射。
改造后的分流代码
有了反射,分流代码变成"先反射、再升级",正则只留给它真正擅长的事——从工单里取出订单号:
import json
import re
from xiangxin import XiangxinClient
client = XiangxinClient()
with open("questions.json", encoding="utf-8") as f:
QUESTIONS = json.load(f)
ORDER_NO = re.compile(r"(?<!\d)\d{8}-\d{4}(?!\d)") # 格式固定的东西,正则依然是最好的工具
REFLEX_OK = 0.8 # 两个阈值都用验证集之外的标注数据确定
S1_OK = 0.7
def route(text: str) -> dict:
m = ORDER_NO.search(text)
order_no = m.group() if m else None
resp = client.system_one(state=text, questions=QUESTIONS, model="xiangxin-reflex:ticket-router")
team = resp.answers["team"]
if team.confidence < REFLEX_OK:
# 反射拿不准:同样的问题交给有世界知识的系统一
resp = client.system_one(state=text, questions=QUESTIONS, model="xiangxin-s1")
team = resp.answers["team"]
if team.confidence < S1_OK:
return {"team": "人工", "order_no": order_no, "by": "human"}
return {
"team": team.choice,
"refund": resp.answers["wants_refund"].noul >= 0.5,
"order_no": order_no,
"by": resp.model,
}新的说法不再需要写规则:把线上被转人工或被纠正的工单补进样本,定期重练即可。
重练
用同一个名字再次 POST /v1/reflexes,就是重练:
- 旧版本在新版本练好之前照常服务,调用不受影响;新版本练好后自动切换。
- 新版本训练失败或被取消时,旧版本继续可用。此时
status为failed或cancelled,而usable仍为true。 - 同一个反射正在排队或训练时再次提交,返回
409 {"detail": "reflex_busy"}。可以等它结束,或先取消。 - 每次重练都要提交完整的样本集,而不是增量;样本不会在象信保存(见数据怎么处理)。
- 重练时可以同时修改问题。新版本练好之前,线上仍是旧版本,请继续用旧的问题调用;看到
status变为ready后再切换到新问题。
取消、列出与删除
# 取消排队中或训练中的任务
curl -X POST https://api.xiangxinai.cn/v1/reflexes/ticket-router/cancel \
-H "Authorization: Bearer $XIANGXIN_API_KEY"
# 列出本组织的所有反射
curl https://api.xiangxinai.cn/v1/reflexes \
-H "Authorization: Bearer $XIANGXIN_API_KEY"
# 删除
curl -X DELETE https://api.xiangxinai.cn/v1/reflexes/ticket-router \
-H "Authorization: Bearer $XIANGXIN_API_KEY"with XiangxinClient() as client:
client.reflexes.cancel("ticket-router")
for r in client.reflexes.list():
print(r.model, r.status, r.usable)
client.reflexes.delete("ticket-router")await client.reflexes.cancel('ticket-router')
for (const r of await client.reflexes.list()) console.log(r.model, r.status, r.usable)
await client.reflexes.delete('ticket-router')- 删除会同时删除反射的权重,之后用这个名字调用返回
404 model_not_found;名字可以重新使用。正在训练的反射被删除时,训练任务一并取消。 - 每个组织最多 20 个反射,超出返回
409 too_many_reflexes;删掉不用的反射即可腾出名额。
在控制台里练
不想写代码,也可以在控制台完成全部操作:打开 控制台 → 条件反射 → 练一个反射,填写名字和说明,在问题编辑器里定义问题(和 Playground 的编辑器一样,带模板),然后上传 .jsonl / .json 样本文件或直接粘贴。控制台会先在浏览器里解析样本,显示条数和前 3 条预览,并标出格式有误的行号;确认无误后点 开始练(免费)。
反射的详情页会显示训练进度、练前 / 练后指标(整体准确率、log loss、ECE 和按问题的准确率)、问题定义,以及可以直接复制的 curl / Python 调用示例。列表页的操作里可以一键在 Playground 试用、重练、取消或删除。
数据怎么处理
- 提交的样本只用于练你的这一个反射,不会用于训练象信一号、基础条件反射或其他任何客户的模型。
- 样本在训练结束后最多保存 30 天,到期自动删除;删除反射时一并删除。开启了零内容留存的组织,训练结束(成功、失败或取消)后立即删除。
- 象信不直接用你的样本训练模型;在保存期内可能参考它们的类型与结构,生成不含原始内容的合成数据来改进模型,见隐私政策。
- 练出来的反射只有你的组织能调用。删除反射即删除其权重。
- 调用反射的推理请求,与调用象信一号的请求适用同样的规则(最多保存 30 天、可开启零内容留存),见隐私政策与数据处理说明。
样本里往往有真实用户的留言。提交前请按最小必要原则处理:去掉判断不需要的姓名、手机号、地址等个人信息,或替换为占位符。
限制与错误
| 项 | 限制 | 超出时 |
|---|---|---|
| 名字 | ^[a-z0-9][a-z0-9-]{0,62}$ | 422 invalid_reflex_name |
说明 description | ≤ 500 字符 | 422 invalid_description |
| 问题数 | ≤ 32 | 422 too_many_questions |
| 样本数 | 10–50,000 | 422 too_few_examples / 422 too_many_examples |
| 请求体 | ≤ 50MB | 413 request_too_large |
| 每组织反射数 | ≤ 20 | 409 too_many_reflexes |
| 同时训练 | 同一个反射同一时间只能有一个训练任务 | 409 reflex_busy |
| 训练服务 | 暂时不可用 | 503 trainer_unavailable,稍后重试 |
完整的接口说明与错误码见 API 参考。

