如何用象信构建
象信一号是用来构建 AI 驱动的软件的,而不是用来构建"自己决定下一步做什么"的智能体。它不写代码,也不规划行动;它提供可以嵌入程序的 AI 原语,让代码始终掌控流程,模型只在需要常识和语言理解的地方给出判断。
一句话总结
先按常规写一个软件流程,只在确实需要 AI 的地方插入象信。
- 控制流、确定性规则和副作用(写库、发消息、扣款)留在代码里;
- 把宽泛的判断拆成窄的、带类型的问题,写清楚指令和选项;
- 每个问题只给它需要的上下文;
- 用概率和置信度决定是执行、请人复核还是升级处理;
- 同一状态下的独立问题一次问完,在代码里组合答案。
三种软件架构
传统软件 LLM 智能体 AI 驱动的软件
──────── ────────── ────────────
代码 → 代码 → 代码 LLM → 工具 → LLM → 工具 → … 代码 → [象信判断] → 代码 → 代码
全部确定、可测试 灵活,但每一轮都可能跑偏 代码掌控流程,AI 只做受约束的单点判断- 传统软件:由大量简单、可靠的原语(比较、分支、循环)组成复杂的决策树。因为每个原语都可靠,才能层层抽象。缺点是遇到非结构化的自然语言就无能为力,只能写越来越脆弱的正则和关键词表。
- LLM 智能体:由大模型读指令、自己选择下一步。在有人盯着的场景下很好用,但每多一轮循环,就多一次偏离轨道的机会,而且很难测试和复现。
- AI 驱动的软件:确定性的工作交给代码,模型只出现在需要"可编程常识"的节点上,而且每个 AI 任务都是原子的、受约束的。这正是象信的设计目标。
为什么象信的答案可以组合
| 性质 | 含义 |
|---|---|
| 结构化 | 答案天然符合你代码期望的类型,永远只在你给的选项上分配概率,不需要从文字里"抠"出结果。 |
| 并行且隔离 | 同一请求里的问题独立评估,一个问题的结果不会成为另一个问题的隐藏上下文。 |
| 可比较 | 输出是数字和枚举,可以直接用于 if、阈值、排序、加权。 |
| 快 | 一次前向即可完成,适合放进实时请求路径。 |
| 置信度 | 以校准为训练目标,用概率表达不确定性,而不是一律自信满满。 |
| 稳定 | 相同输入会得到几乎相同的输出(受 GPU 数值噪声影响,末位可能有 ±0.01 左右的浮动,见 已知短板)。 |
设计一个象信工作流
1. 能用代码就用代码
确定性的工作交给代码:可靠、免费、可测试。不要为了"智能"而把本可以写成 if 的逻辑交给模型,也尽量不要用智能体式的 while 循环去做一个普通流程就能完成的事。
from datetime import date
days_overdue = (date.today() - invoice_due_date).days
if days_overdue > 30:
send_to_collections(invoice_id)逾期天数是算出来的,不是"判断"出来的。象信只该出现在代码无法胜任的地方,比如"催款回信里,客户是否在承诺具体的还款日期?"。
更多经过验证的组合方式见 模式。
2. 拆分状态:只给相关的上下文
状态里只放当前问题需要的材料,避免无关内容干扰判断。需要最新信息时,从你自己的知识库或数据库里查出来放进状态,而不要指望模型"记得"。
{
"state": {
"ticket_message": "航班被取消了,机票钱能退吗?",
"refund_policy": "因航空公司原因取消的航班,可全额退款,不收取手续费。"
},
"model": "xiangxin-latest",
"questions": {
"policy_supports_refund": {
"type": "noul",
"instructions": "`refund_policy` 是否支持 `ticket_message` 中提出的退款要求?"
}
}
}这里没有把整本客服手册塞进去,只放了与退款相关的那一条规则。
3. 让状态有结构
state 和问题都可以是嵌套的 JSON。当状态包含多个部分时,在问题里用带反引号的点号加下标路径指明要看哪里,可以消除歧义:
{
"state": {
"support": {
"tickets": [
{"message": "订单 A-104 被扣了两次款。"},
{"message": "怎么修改登录密码?"}
]
},
"commerce": {
"orders": [
{"id": "A-104", "charges": [
{"amount_yuan": 199, "status": "已扣款"},
{"amount_yuan": 199, "status": "已扣款"}
]}
]
},
"account": {
"security": {"password_reset": "在 App「我的 → 设置 → 账号安全」中通过短信验证修改密码。"}
}
},
"model": "xiangxin-latest",
"questions": {
"duplicate_charge": {
"type": "noul",
"instructions": "`support.tickets[0].message` 和 `commerce.orders[0].charges` 是否表明发生了重复扣款?"
},
"reset_doc_answers": {
"type": "noul",
"instructions": "`account.security.password_reset` 能否解决 `support.tickets[1].message` 中的问题?"
}
}
}4. 拆分问题:原子化、具体化
这是整篇指南里最重要的一条。 宽泛的问题把好几个判断藏在一个答案背后;原子化的问题把它们一一暴露出来,你可以逐个检查、调整,再在代码里组合。
以识别钓鱼短信为例。一个宽泛的问题:
{
"is_scam": {"type": "noul", "instructions": "`sms` 是诈骗短信吗?"}
}模型当然能给出一个概率,但当它错了,你不知道错在哪里,也没法按业务需要调整。拆开之后:
{
"requests_code": {
"type": "noul",
"instructions": "`sms.body` 是否要求收件人提供验证码、密码或银行卡号?"
},
"unexpected_money": {
"type": "noul",
"instructions": "`sms.body` 是否声称收件人获得了意外的奖金、退款或补贴?"
},
"time_pressure": {
"type": "noul",
"instructions": "`sms.body` 是否催促收件人在很短时间内采取行动?"
},
"sender_mismatch": {
"type": "noul",
"instructions": "`sms.signature` 中自称的机构,与 `sms.sender_number` 的号码类型是否不符(例如银行却用个人手机号发送)?"
},
"link_mismatch": {
"type": "noul",
"instructions": "`sms.links[0]` 的域名是否与 `sms.signature` 自称的机构无关?"
}
}每个问题都只关心一个可观察的特征。你可以分别给它们设阈值、加权,也可以在某个特征频繁出错时单独改写它的措辞。经验上,给模型结构化的状态加上几个窄问题,效果往往远好于一大段原文加一个宽泛的问题。
同样的道理适用于检查大模型的工具调用:与其问"这串工具调用对不对?",不如分别问"第一个调用的工具是否适合解析用户给出的城市?""第二个调用的日期参数是否与用户要求的日期一致?""温度单位是否与用户要求一致?"——每一个都能单独核查。
5. 让问题也有结构
instructions 和 criteria 通常是字符串,一个简短、无歧义的问题用字符串就够了。但它们也可以是对象或数组:把问题放在一个字段,把指导问题的数据放在其他字段。
以下情况适合用结构:
- 问题需要背景或示例。 一长串背景说明或示例输入,放进与问题并列的命名字段里,代码可以随时增删替换,而不必重写问题本身。
- 问题的一部分来自代码。 从数据库取出的值,放进独立字段,而不是拼接进字符串模板。
- 多个问题措辞相似。 通过附加的数据字段让它们彼此区分。
例如,判断一份简历是否与人才库里的某条已有记录是同一个人:
{
"same_as_record_1832": {
"type": "noul",
"instructions": {
"existing_record": {"name": "李文博", "city": "杭州", "last_employer": "某电商公司"},
"question": "`resume` 与 `existing_record` 是否是同一个人?"
}
}
}existing_record 由代码从数据库填入,可以随时替换;question 通过反引号引用它。
criteria 里的每个描述也可以是对象。对于容易混淆的 Choice 选项,可以用统一的字段写清楚"覆盖什么""不包括什么""典型例子",让模型直接对比:
{
"card_topic": {
"type": "choice",
"instructions": "用户在咨询虚拟信用卡的哪方面问题?",
"criteria": {
"apply": {
"what": "申请条件、开通方式、用途",
"not_for": "额度、次数、使用范围的限制",
"examples": ["虚拟卡怎么开通?", "虚拟卡有什么用?"]
},
"limits": {
"what": "每日可生成数量、单笔额度、可用商户范围",
"not_for": "如何开通",
"examples": ["一天最多能生成几张虚拟卡?", "虚拟卡能在境外网站用吗?"]
}
}
}
}各原语页面都有更完整的结构化示例,所有支持结构的位置见 进阶:结构化。
6. 多问问题
对同一份状态,一次请求里尽可能多地提出窄而独立的问题。问题是并行评估的,只多付少量问题本身的 token,不增加串行的往返次数——这是在象信上获得"单位成本智能"最大化的方法。即使某些问题只对部分输入有意义,也可以先问了再说,由代码决定用哪些答案。见 推测式扇出。
7. 在代码里组合答案
用确定性规则或加权求和把独立的信号组合起来。如果你有标注数据,也可以把这些概率当作特征,训练一个传统的机器学习模型(逻辑回归、梯度提升树等)来做最终判断。
a = resp.answers
# 把几个独立信号组合成一个业务上的"回答质量分"
quality = (
0.4 * a["answers_the_question"].noul
+ 0.4 * a["claims_supported"].noul
+ 0.2 * (1 - a["contradicts_context"].noul)
)见 组合评分。
8. 按不确定性路由
让代码对"有把握"和"没把握"的答案采取不同动作:拿不准的交给人工或更贵的推理模型。阈值不要拍脑袋,而是在你的数据上画出"置信度—准确率"曲线来确定。
topic = resp.answers["card_topic"]
if topic.confidence < 0.8:
send_to_human_review(ticket_id)
else:
dispatch(topic.choice, ticket_id)提示
拆分问题并不意味着更多的往返。同一状态上的问题在一次请求里并行完成。
用评估集迭代
问题的措辞和阈值,是象信应用里最需要打磨、也最值得人来审阅的部分。建议这样迭代:
- 攒一个小评估集。 从真实数据里抽 100~300 条,人工标注你期望的答案。边界样本和易错样本要刻意多放一些。
- 跑一遍,看错例。 对每个错例问自己:"我心里想问的是什么?"——你为解释错例而说出的那句话,往往就是指令里缺的那半句。把它补进
instructions或criteria。 - 看概率而不仅是对错。 把答案按置信度分桶,看每个桶的准确率。高置信度桶的准确率决定了你能放心自动化多少流量。
- 把问题和阈值集中管理。 放在一个文件里(例如
questions.py),代码评审时一眼就能看到改了什么。 - 固定模型版本做对比。 调好的阈值是针对某个具体版本的;需要严格复现时,在请求里写
xiangxin-1.0.0而不是别名xiangxin-latest,在自己的节奏下升级。
综合示例
下面这个工单分诊函数把上面的要点串在一起:确定性状态由代码处理,只把相关的结构化上下文发给模型,一次请求问多个原子问题,再用明确的置信度门槛组合答案。
from xiangxin import Choice, Noul, Score, XiangxinClient
# ---- 问题与阈值集中定义,方便评审 ----
TOPIC_MIN_CONFIDENCE = 0.75
SCAM_SIGNAL_THRESHOLD = 0.7
ANGER_ESCALATE = 1.5
QUESTIONS = {
"topic": Choice(
instructions={
"question": "`ticket.message` 应该由哪个组处理?",
"focus": "按顾客的主要诉求分类。",
},
criteria={
"billing": {
"what": "扣款、发票、退款、会员续费",
"not_for": "物流查询或账号登录",
"examples": ["被扣了两次钱", "退款什么时候到账?"],
},
"orders": {
"what": "订单状态、配送、取消、退货",
"not_for": "扣款或账号问题",
"examples": ["我的快递到哪了?", "帮我取消订单"],
},
"account": {
"what": "登录、实名、绑定手机、账号安全",
"not_for": "扣款或物流",
"examples": ["收不到验证码", "账号被冻结了"],
},
},
),
"asks_for_code": Noul(
instructions="`ticket.message` 是否在向客服索要或诱导提供验证码、密码?",
),
"claims_staff_identity": Noul(
instructions="发件人是否自称平台工作人员,并要求用户转账或提供账号信息?",
),
"anger": Score(
instructions="`ticket.message` 中顾客的愤怒程度如何?",
criteria=["平静", "有些不满", "明显愤怒", "扬言投诉或曝光"],
),
"mentions_open_order": Noul(
instructions="`ticket.message` 提到的问题是否与 `customer.open_orders` 中的某个订单有关?",
),
}
def triage_ticket(client: XiangxinClient, ticket: dict, customer: dict) -> str:
# 确定性情况:不调用模型
if ticket["status"] == "closed":
return "no_action"
open_orders = [o for o in customer["orders"] if o["status"] != "已签收"]
# 只放问题需要的上下文
state = {
"ticket": {"message": ticket["message"]},
"customer": {"level": customer["level"], "open_orders": open_orders},
}
resp = client.system_one(state=state, questions=QUESTIONS)
a = resp.answers
# 安全信号优先
scam_signals = max(a["asks_for_code"].noul, a["claims_staff_identity"].noul)
if scam_signals > SCAM_SIGNAL_THRESHOLD:
return "security_review"
# 情绪激烈的先安抚
if a["anger"].score >= ANGER_ESCALATE:
return "senior_agent"
# 分类不确定就交给人
if a["topic"].confidence < TOPIC_MIN_CONFIDENCE:
return "human_triage"
return "queue_" + a["topic"].choice
if __name__ == "__main__":
with XiangxinClient() as client:
route = triage_ticket(
client,
ticket={"status": "open", "message": "我昨天买的耳机到现在还没发货,能不能快点?"},
customer={"level": "PLUS", "orders": [{"id": "A-2091", "status": "待发货"}]},
)
print(route) # 例如:queue_orders这段代码里,AI 只负责五个一眼能答的判断;路由规则、阈值、优先级全部是普通代码,可以测试、可以评审、可以随时调整。

