Commit 9765bb32 by luoqi

refactor(persona): 结构化特征值 evidence.data → 独立 persona_features.data 列

evidence 语义是"为什么(证据溯源)",塞业务 payload 越界。改用独立 data Json? 列:
- schema: persona_features 加 data Json?(+ migration 20260602054612)
- PersonaFeatureDraft.data 顶层字段(从 evidence.data 移出);evidence 回归只放 factIds
- persona.service 写入 + 序列化透出 data 列
- entitlement extractor 结构化明细写 data 列

本地验证(6857):data 列含 {commercialInsurers/lastCommercialInsuranceAt/...},evidence 只剩 factIds。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 69eede94
-- AlterTable
ALTER TABLE "persona_features" ADD COLUMN "data" JSONB;
...@@ -602,6 +602,13 @@ model PersonaFeature { ...@@ -602,6 +602,13 @@ model PersonaFeature {
/// 可选数值评分,用于排序/筛选 /// 可选数值评分,用于排序/筛选
score Float? score Float?
/// 结构化 payload(可空) 特征值里"需要被 scorer / UI 结构化消费"的明细。
/// description(人读串)/ score(排序数)分工:这里放机器要 parse 的对象。
/// :权益身份 { commercialInsurers:[...], lastCommercialInsuranceAt, monthsSinceLastCommercial }
/// evidence 区分:evidence = 为什么(证据溯源),data = 是什么(结构化特征值)
/// 形状由各 feature 应用层自管(zod);DB 不强约束(db-review §6 JSON 列纪律)
data Json?
/// 证据引用:{ factIds, agentInvocationIds, ruleIds, promptVersion } /// 证据引用:{ factIds, agentInvocationIds, ruleIds, promptVersion }
/// 任何 persona 都能反查到"为什么得出这个结论" /// 任何 persona 都能反查到"为什么得出这个结论"
/// 引证 fact 即可,fact 自带 transactionIds 可两跳回溯到原始 transaction /// 引证 fact 即可,fact 自带 transactionIds 可两跳回溯到原始 transaction
......
...@@ -118,16 +118,17 @@ export class EntitlementStatusFeatureExtractor implements FeatureExtractor { ...@@ -118,16 +118,17 @@ export class EntitlementStatusFeatureExtractor implements FeatureExtractor {
description: parts.join(';'), description: parts.join(';'),
// score = 商保最近月数(越小越新);仅医保时用医保月数兜底。旗标型,非梯度,scorer 读 data 更精确。 // score = 商保最近月数(越小越新);仅医保时用医保月数兜底。旗标型,非梯度,scorer 读 data 更精确。
score: monthsCommercial ?? monthsMedical ?? null, score: monthsCommercial ?? monthsMedical ?? null,
// 结构化特征值 → persona_features.data 列(给 scorer/UI 结构化消费)
data: {
commercialInsured: hasCommercial,
commercialInsurers: insurers, // 阶段2 re-ingest 后才有保司名
lastCommercialInsuranceAt: lastCommercialAt ? lastCommercialAt.toISOString() : null,
monthsSinceLastCommercial: monthsCommercial,
medicalInsured: hasMedical,
lastMedicalInsuranceAt: lastMedicalAt ? lastMedicalAt.toISOString() : null,
},
evidence: { evidence: {
factIds: [...commercialFactIds, ...medicalFactIds], factIds: [...commercialFactIds, ...medicalFactIds],
data: {
commercialInsured: hasCommercial,
commercialInsurers: insurers, // 阶段2 re-ingest 后才有保司名
lastCommercialInsuranceAt: lastCommercialAt ? lastCommercialAt.toISOString() : null,
monthsSinceLastCommercial: monthsCommercial,
medicalInsured: hasMedical,
lastMedicalInsuranceAt: lastMedicalAt ? lastMedicalAt.toISOString() : null,
},
}, },
}; };
} }
......
...@@ -53,13 +53,13 @@ export interface PersonaFeatureDraft { ...@@ -53,13 +53,13 @@ export interface PersonaFeatureDraft {
key: PersonaFeatureKey; key: PersonaFeatureKey;
description: string; description: string;
score?: number | null; score?: number | null;
/// 结构化特征值(可空)→ 落 persona_features.data 列。description 人读、score 排序、
/// data 给 scorer / UI 结构化消费(如权益身份 insurers / lastInsuranceAt / monthsSinceLast)。
data?: Record<string, unknown> | null;
/// 证据反查 fact + AI 调用;**不存 ruleIds 字符串常量**(版本追溯靠 git + persona.createdAt) /// 证据反查 fact + AI 调用;**不存 ruleIds 字符串常量**(版本追溯靠 git + persona.createdAt)
evidence: { evidence: {
factIds: string[]; factIds: string[];
agentInvocationIds?: string[]; agentInvocationIds?: string[];
/// 结构化 payload(零迁移路线):description 是人读串、score 是排序数,
/// 需要被 scorer / UI 结构化消费的明细放这里(如权益身份 insurers / lastInsuranceAt)。
data?: Record<string, unknown>;
}; };
} }
......
...@@ -220,6 +220,10 @@ export class PersonaService { ...@@ -220,6 +220,10 @@ export class PersonaService {
key: d.key, key: d.key,
description: d.description, description: d.description,
score: d.score ?? null, score: d.score ?? null,
data:
d.data == null
? undefined // nullable Json 列省略 → NULL
: (d.data as unknown as Prisma.InputJsonValue),
evidence: d.evidence as unknown as Prisma.InputJsonValue, evidence: d.evidence as unknown as Prisma.InputJsonValue,
})), })),
}, },
...@@ -298,6 +302,7 @@ export class PersonaService { ...@@ -298,6 +302,7 @@ export class PersonaService {
key: string; key: string;
description: string; description: string;
score: number | null; score: number | null;
data: unknown;
evidence: unknown; evidence: unknown;
createdAt: Date; createdAt: Date;
}>; }>;
...@@ -318,6 +323,7 @@ export class PersonaService { ...@@ -318,6 +323,7 @@ export class PersonaService {
key: f.key, key: f.key,
description: f.description, description: f.description,
score: f.score, score: f.score,
data: (f.data as Record<string, unknown>) ?? null,
evidence: (f.evidence as Record<string, unknown>) ?? {}, evidence: (f.evidence as Record<string, unknown>) ?? {},
createdAt: f.createdAt.toISOString(), createdAt: f.createdAt.toISOString(),
})), })),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment