让 AI 真正“看懂“业务数据:基于注解的对象自然语言转译方案
让 AI 真正"看懂"业务数据:基于 @Tips 注解的对象自然语言转译方案
摘要: 在企业级智能助手、智能客服、MCP Server 等 Agent 场景中,后端接口返回的 DTO/BO/VO 对象(或 JSON)对大语言模型而言是"冷数据"——字段名与技术语义无法直接映射到业务含义。这里就当前AI读取接口返回"属性名-属性值"形式的结构化对象数据可能存在理解偏差的问题,实现一种基于自定义
@Tips注解的轻量级转译方案,将结构化对象一键转换为自然语言表达式(如订单ID:12345;订单金额:99.99元;订单状态:已结束),无需额外的字典查询步骤,无需AI二次转义,即可显著提升 AI 的语义识别准确率与响应可靠性。
一、问题背景:AI 为什么"看不懂"我们的 DTO
在企业智能体系统中,MCP Tool 或内部 CLI 调用的返回数据通常长这样:
{
"orderId": 1937384137282740225,
"operatorId": 10086,
"status": "ENDED",
"moneyAmount": 99.99,
"timeEnd": "2026-04-01 12:00:00",
"stopDesc": "用户主动结束"
}
直接把这段 JSON 塞给 AI,会产生几个问题:
- 字段名 ≠ 业务语义 —— AI 不知道
operatorId是"运营商ID"还是"操作员ID" - 枚举值丢失可读性 ——
status: "ENDED"对 AI 来说只是一个字符串,不是"已结束" - 数值缺少单位 ——
99.99是元?是度电?AI 无法判断 - 关联 ID 无法翻译 —— 即使额外传入数据字典,多一步 API 调用就多一分延迟和失败风险
- 嵌套对象膨胀 —— 深层嵌套的 JSON 会消耗大量 token,且 AI 容易遗漏关键字段
传统的做法是:先返回 JSON → 再查字典翻译字段 → 拼成自然语言给 AI。链路长、易出错。
能不能在数据返回的同一时刻,直接生成 AI 可读的自然语言?
这就是 @Tips 注解方案的出发点。
二、核心思路:注解驱动的一次性转译
设计目标很简单:
开发者在 DTO 字段上加一行注解,运行时一行代码调用,即可得到 AI 友好的自然语言描述。
2.1 @Tips 注解定义
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tips {
/** 属性的中文或自然语言描述名称 */
String name();
/** 枚举值解释属性名,如 enumDesc="desc" 表示取枚举的 desc 字段 */
String enumDesc() default "";
/** 单位,用于数值后附加,如"元"、"kg" */
String unit() default "";
/** 日期格式,如"yyyy-MM-dd HH:mm:ss" */
String dateFormat() default "";
/** 额外解释说明 */
String explain() default "";
/** 嵌套对象的最大展开深度,默认 3 */
int maxDepth() default 3;
/** 名称策略键,用于将 ID 值翻译为名称(如运营商ID→运营商名称) */
String nameStrategy() default "";
/** 是否脱敏(中间 1/3 信息用 * 替代) */
boolean desensitization() default false;
/** 隐藏值,匹配则不输出该字段 */
String hiddenValue() default "";
}
注解设计遵循"一个注解覆盖所有常见转译需求"的原则,字段各司其职,不多不少。
2.2 使用示例
@Setter
@Getter
public class BizOrderDTO {
@Tips(name = "订单ID")
private Long orderId;
@Tips(name = "运营商", nameStrategy = "OPERATOR_TITLE")
private Long operatorId;
@Tips(name = "订单状态", enumDesc = "desc")
private OrderStatusEnum status;
@Tips(name = "结束时间", dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date timeEnd;
@Tips(name = "结束原因描述", explain = "仅用于客户端数据展示")
private String stopDesc;
@Tips(name = "订单金额", unit = "元")
private BigDecimal moneyAmount;
@Tips(name = "子订单")
private SubOrderDTO subOrder;
}
调用方式只有一行:
String result = objectToTipsManager.toTipsExpression(order);
输出结果:
订单ID:12345;运营商:XX充电;订单状态:已结束;结束时间:2026-04-01 12:00:00;
结束原因描述:用户主动结束 仅用于客户端数据展示;订单金额:99.99元;
子订单:{子订单ID:2;子订单名称:测试子订单;子订单金额:50.00元}
三、实现架构
3.1 核心转换流程
toTipsExpression(obj)
├── obj 是数组 → formatArray() → 转为 [元素1,元素2,...]
├── obj 是集合 → formatCollection() → 转为 [元素1,元素2,...]
├── obj 是 Map → formatMap() → 转为 {key1:value1,key2:value2}
└── obj 是普通对象
├── 遍历所有字段(含父类)
│ ├── 有 @Tips → processField()
│ │ ├── hiddenValue 匹配 → 跳过
│ │ ├── nameStrategy → 调 NameStrategyHandler 翻译
│ │ ├── formatValue() → 按类型格式化
│ │ │ ├── 枚举 → formatEnum()
│ │ │ ├── Date → formatDate()
│ │ │ ├── Number → formatNumber() + unit
│ │ │ ├── 数组/集合/Map → 递归
│ │ │ └── 嵌套对象 → formatNestedObject() 递归
│ │ ├── desensitization → maskMiddle()
│ │ └── explain → 追加说明文本
│ └── 无 @Tips → 忽略
└── 用 ";" 拼接所有字段 → "字段1:值1;字段2:值2"
3.2 核心代码
@Slf4j
@Component
public class ObjectToTipsManager {
@Autowired(required = false)
private NameStrategyHandler nameStrategyHandler;
public String toTipsExpression(Object obj) {
return toTipsExpression(obj, 0, 3);
}
public String toTipsExpression(Object obj, int currentDepth, int maxDepth) {
if (obj == null) {
return "";
}
if (currentDepth > maxDepth) {
return toJsonString(obj);
}
// 集合/数组/Map 入口
if (obj.getClass().isArray()) {
return formatArray(obj, currentDepth, maxDepth);
}
if (obj instanceof Collection) {
return formatCollection((Collection<?>) obj, currentDepth, maxDepth);
}
if (obj instanceof Map) {
return formatMap((Map<?, ?>) obj, currentDepth, maxDepth);
}
// 普通对象:反射遍历 @Tips 字段
List<Field> allFields = getAllFields(obj.getClass());
List<String> parts = new ArrayList<>();
for (Field field : allFields) {
field.setAccessible(true);
Tips tips = field.getAnnotation(Tips.class);
if (tips != null) {
Object value = field.get(obj);
String part = processField(field, value, tips, currentDepth, maxDepth);
if (!part.isEmpty()) {
parts.add(part);
}
}
}
return parts.isEmpty() ? toJsonString(obj) : String.join(";", parts);
}
private String processField(Field field, Object value, Tips tips,
int currentDepth, int maxDepth) {
// hiddenValue 匹配则跳过
if (tips.hiddenValue() != null && !tips.hiddenValue().isEmpty()
&& isHiddenValue(value, tips.hiddenValue())) {
return "";
}
// nameStrategy 翻译 ID 为名称
Object processedValue = value;
if (!tips.nameStrategy().isEmpty() && nameStrategyHandler != null) {
processedValue = nameStrategyHandler.switchName(tips.nameStrategy(), value);
}
// 格式化值
String fieldValue = formatValue(processedValue, field.getType(), tips,
currentDepth, maxDepth);
String result = tips.name() + ":" + fieldValue;
// 脱敏
if (tips.desensitization()) {
result = maskMiddle(result);
}
// 追加解释
if (!StrUtil.isBlank(tips.explain())) {
result = result + " " + tips.explain();
}
return result;
}
// ... formatValue / formatEnum / formatDate / formatNumber 等见下文
}
3.3 集合/数组/Map 支持
对于 AI 场景中最常见的列表返回(如 List<BizOrderDTO>),需要特殊处理:
/** 格式化 Collection(List、Set 等) */
private String formatCollection(Collection<?> collection, int currentDepth, int maxDepth) {
if (collection.isEmpty()) {
return "[]";
}
List<String> elements = new ArrayList<>();
for (Object item : collection) {
String itemStr = toTipsExpression(item, currentDepth + 1, maxDepth);
if (!itemStr.isEmpty()) {
elements.add(itemStr);
}
}
return elements.isEmpty() ? "[]" : "[" + String.join(",", elements) + "]";
}
/** 格式化数组 */
private String formatArray(Object array, int currentDepth, int maxDepth) {
List<?> list;
if (array instanceof Object[]) {
list = Arrays.asList((Object[]) array);
} else if (array instanceof int[]) {
list = Arrays.asList(((int[]) array));
} // ... 其他基本类型数组略
else {
return toJsonString(array);
}
return formatCollection(list, currentDepth, maxDepth);
}
/** 格式化 Map */
private String formatMap(Map<?, ?> map, int currentDepth, int maxDepth) {
if (map.isEmpty()) {
return "{}";
}
Map<Object, String> converted = new LinkedHashMap<>();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String valueStr = toTipsExpression(entry.getValue(), currentDepth + 1, maxDepth);
converted.put(entry.getKey(), valueStr.isEmpty() ? String.valueOf(entry.getValue()) : valueStr);
}
List<String> parts = new ArrayList<>();
for (Map.Entry<Object, String> entry : converted.entrySet()) {
parts.add(entry.getKey() + ":" + entry.getValue());
}
return "{" + String.join(",", parts) + "}";
}
3.4 值格式化与嵌套对象
private String formatValue(Object value, Class<?> type, Tips tips,
int currentDepth, int maxDepth) {
if (value != null && type.isArray()) {
return formatArray(value, currentDepth, maxDepth);
}
if (value instanceof Collection) {
return formatCollection((Collection<?>) value, currentDepth, maxDepth);
}
if (value instanceof Map) {
return formatMap((Map<?, ?>) value, currentDepth, maxDepth);
}
if (type.isEnum()) {
return formatEnum(value, tips);
}
if (value instanceof Date) {
return formatDate((Date) value, tips);
}
if (value instanceof Number) {
return formatNumber((Number) value, tips);
}
if (!isPrimitiveType(type) && !type.equals(String.class)) {
return formatNestedObject(value, tips, currentDepth, maxDepth);
}
return String.valueOf(value);
}
/** 嵌套对象递归 */
private String formatNestedObject(Object obj, Tips tips,
int currentDepth, int maxDepth) {
int newDepth = currentDepth + 1;
int fieldMaxDepth = tips.maxDepth() > 0 ? tips.maxDepth() : maxDepth;
String nested = toTipsExpression(obj, newDepth, Math.min(fieldMaxDepth, maxDepth));
return nested.isEmpty() ? toJsonString(obj) : "{" + nested + "}";
}
3.5 脱敏处理
对于手机号、身份证等敏感字段,注解支持一键脱敏:
@Tips(name = "联系电话", desensitization = true)
private String phone;
public static String maskMiddle(String input) {
if (input == null || input.length() <= 1) return input;
int len = input.length();
if (len == 2) return input.charAt(0) + "***";
int keepEachSide = Math.min(len / 3, 3);
return input.substring(0, keepEachSide) + "***" + input.substring(len - keepEachSide);
}
// "13800138000" → "138***8000"
3.6 hiddenValue:智能过滤零值/默认值
业务对象中大量存在 status = 0、deleted = false 等默认值字段,对 AI 无意义且浪费 token:
@Tips(name = "是否删除", hiddenValue = "false")
private Boolean deleted;
@Tips(name = "状态码", hiddenValue = "0")
private Integer status;
private boolean isHiddenValue(Object value, String hiddenValue) {
if (value instanceof Number) {
// BigDecimal 数值比较,解决 "0" vs "0.00" 精度问题
BigDecimal decimalValue = new BigDecimal(String.valueOf(value));
BigDecimal decimalHidden = new BigDecimal(hiddenValue);
return decimalValue.compareTo(decimalHidden) == 0;
}
if (value instanceof Boolean) {
// 支持 "true"/"false" 或 "0"/"1" 匹配
Boolean boolVal = (Boolean) value;
if ("true".equalsIgnoreCase(hiddenValue) || "1".equals(hiddenValue)) {
return Boolean.TRUE.equals(boolVal);
}
if ("false".equalsIgnoreCase(hiddenValue) || "0".equals(hiddenValue)) {
return Boolean.FALSE.equals(boolVal);
}
return String.valueOf(value).equals(hiddenValue);
}
return String.valueOf(value).equals(hiddenValue);
}
四、实际效果对比
4.1 单对象场景
| 维度 | 传统 JSON 返回 | @Tips 转译 |
|---|---|---|
| 原始数据 | {"orderId":12345,"status":"ENDED","moneyAmount":99.99} |
订单ID:12345;订单状态:已结束;订单金额:99.99元 |
| AI 理解成本 | 需猜测字段含义、枚举值含义 | 直接可读 |
| 单位信息 | 丢失 | 保留(“元”) |
| 枚举可读性 | 不可读(“ENDED”) | 可读(“已结束”) |
| Token 消耗 | 中等 | 相近或更少(去除了无意义字段) |
4.2 列表场景
// 传统 JSON —— AI 需要在冗长的 JSON 中解析
[{"orderId":1001,"status":"ENDED","moneyAmount":50.00},
{"orderId":1002,"status":"IN_PROGRESS","moneyAmount":99.99}]
// @Tips 转译 —— 一目了然
[{订单ID:1001;订单状态:已结束;订单金额:50.00元},
{订单ID:1002;订单状态:进行中;订单金额:99.99元}]
4.3 嵌套场景
// 传统 JSON —— 深层嵌套容易丢失关键信息
{"orderId":1,"subOrder":{"subId":2,"subName":"测试","subAmount":50.00}}
// @Tips 转译 —— 结构化自然语言
订单ID:1;子订单:{子订单ID:2;子订单名称:测试;子订单金额:50.00元}
4.4 MCP Server 中的集成
在 MCP Tool 接口中,只需一行替换即可生效:
@ToolMapping(name = "getOrderDetail", title = "查询用户订单信息")
public String getOrderDetail(@Param(description = "订单号") String orderSeq) {
BizOrderDTO order = orderHelper.queryOrder(orderSeq);
// 原来:return JSON.toJSONString(order);
// 现在:
return objectToTipsManager.toTipsExpression(order);
}
@ToolMapping(name = "getOrderList", title = "查询订单列表")
public String getOrderList(@Param(description = "用户ID") Long userId) {
List<BizOrderDTO> orders = orderHelper.queryOrders(userId);
// 直接支持 List 转换
return objectToTipsManager.toTipsExpression(orders);
}
五、为什么比"先返回 JSON + 后查字典"更好
| 对比维度 | JSON + 字典方案 | @Tips 转译方案 |
|---|---|---|
| 调用链路 | AI 收到 JSON → 识别需要翻译的字段 → 调用字典 API → 拼接自然语言 | 一次调用直接得到自然语言 |
| 延迟 | 至少多一次字典查询 RT | 无额外 RT,纯内存反射转换 |
| 可靠性 | 字典查询可能失败、超时、数据不一致 | 确定性输出,不依赖外部服务 |
| 可控性 | 字典数据可能被意外修改 | 注解即文档,改动需发版 |
| Token 效率 | 原始 JSON + 字典数据双重 token 消耗 | 仅输出有效信息,token 更精简 |
| 开发成本 | 需要维护字典映射关系、处理异常分支 | 加注解即可,零额外代码 |
核心优势用一句话总结:
把"翻译"的工作从 AI 运行时转移到了开发者定义时,用确定的注解代替不确定的运行时查询。
六、适用场景与最佳实践
6.1 适用场景
- MCP Server 的 Tool 返回值转译
- 智能客服/助手对话中的数据展示
- RAG 系统中结构化数据向量化前的预处理
- 任何需要将 DTO/BO/VO 转为自然语言的 AI 交互场景
6.2 最佳实践
- 核心字段优先 —— 不要给所有字段加注解,只标注 AI 需要理解的关键业务字段
- 合理控制嵌套深度 —— 默认 3 层足够,过深会膨胀输出且 AI 难以理解
- 善用 hiddenValue —— 过滤掉
deleted=false、version=0等对 AI 无意义的默认值 - nameStrategy 解耦翻译逻辑 —— ID 翻译走统一的 Handler,避免硬编码
- 脱敏字段及时开启 —— 手机号、身份证号等敏感数据开启
desensitization = true
七、总结
@Tips 注解方案的核心价值在于用最小的开发成本,把后端数据变成 AI 真正能"理解"的自然语言。它不依赖任何 AI 框架,不增加调用链路,只是一个纯反射的字符串转换工具,但在企业智能体场景中能显著降低 AI 的"理解偏差"。
开源工程 base-ai-assistant
基于spring-boot、spring-ai、spring-ai-alibaba实现的RAG、MCP、Agent智能体基础服务框架应用;智能客服、智能运维、智能助手、简单工作流/垂直领域智能体的基础应用架构版本,按需拓展。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)