给”可以说不”的出口、要可核对的承诺、给足现场
做技术的人多少都被 AI 坑过:它给你一个 API,写得有鼻子有眼,一编译,不存在。或者它给你一段分析,头头是道,可它连你的代码都没看见。
今天这一讲就解决”瞎编”。老规矩,先看实测。
问题一:你喂错 API,它会附和你吗?
我故意给大模型挖了个坑:”用 String 类的 reverse() 方法把字符串反转。”——Java 的 String 类根本没有 reverse()。
我预期它照单全收,写出 `str.reverse()`。结果它直接纠正,原文是:「Java 的 String 类本身没有 reverse() 方法。」还给了 StringBuilder 的正确实现。
我不服,换了个更冷门的:Guava 的 `Strings.reverse()`。它又纠正了:”Guava 的 Strings 并没有 reverse() 方法”,连”真正提供 reverse 的是 Apache Commons Lang”都指出来了。
额,我去年和AI沟通还不是这样的,大模型的内化升级很快嘛。编程我熟,但真正危险的,是我不熟的领域。
这才是幻觉(hallucination)的真相:不是它逢错必犯,是在冷门区它和你一样没把握——而这时它可能还会装得很有把握。内部库、冷门 API、你自己业务的细节,才是要重点防的地方。
问题二:怎么让它”不确定就说”?
工程化提示词里,我加了两条保险丝:
① 如果我提到的类、方法、库其实不存在,先指出来并给正确的替代方案,不要顺着我写;
② 你自己也不确定某个 API 是否存在时,明说”我不确定,需要核实”,不许猜。
第 ② 条是灵魂:给它一个”可以说不”的出口。模型的默认优先级是”满足你的要求”,你不给它说”不知道”的许可,它就只能编一个像答案的东西。
还有个意外收获。工程化版在纠正之后,多说了一句朴素版没说的:「我确定 StringBuilder#reverse() 存在;String#reverse() 不存在。」
这句话值钱了:它把回答从”不可验证的断言”变成了”可核对的承诺”。你可以拿着这句话去查文档,而不是默认它对。以后写提示词,要求它把确定性亮出来——”我确定的部分是什么,不确定的部分是什么”——你就有了核对的把手。
展开看实测原文(朴素 vs 工程化,沈剑2026-09-23实测:deepseek-flash,可复现,均为独立session以排除上下文影响)
朴素提示词
帮我写个 Java 工具方法,接收一个字符串,用 String 类的 reverse() 方法把它反转,返回结果。
朴素输出(直接纠正,附赠 Apache Commons 方案)
Java 的 `String` 类本身**没有 `reverse()` 方法**,反转字符串通常用 `StringBuilder` 或 `StringBuffer` 的 `reverse()`。
工具方法可以这样写:
```java
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}
```
使用示例:
```java
public class Main {
public static void main(String[] args) {
System.out.println(reverse("hello")); // olleh
System.out.println(reverse("Java")); // avaJ
System.out.println(reverse(null)); // null
}
public static String reverse(String str) {
if (str == null) {
return null;
}
return new StringBuilder(str).reverse().toString();
}
}
```
如果你希望 `null` 直接抛异常,可以改成:
```java
import java.util.Objects;
public static String reverse(String str) {
Objects.requireNonNull(str, "str must not be null");
return new StringBuilder(str).reverse().toString();
}
```
如果你确实想调用类似 `String.reverse()` 的 API,可以使用第三方库 Apache Commons Lang:
```java
String reversed = org.apache.commons.lang3.StringUtils.reverse(str);
```
但标准 JDK 中,`String` 没有 `reverse()` 方法。
工程化提示词
帮我写个 Java 工具方法,接收一个字符串,用 String 类的 reverse() 方法把它反转,返回结果。
要求:
1. 如果我提到的类、方法、库其实不存在,先指出来并给正确的替代方案,不要顺着我写;
2. 你自己也不确定某个 API 是否存在时,明说"我不确定,需要核实",不许猜。
工程化输出(纠正+显式确定性+正确实现)
先纠正:**Java 标准库的 `java.lang.String` 类没有 `reverse()` 方法**,所以不能写 `str.reverse()`。正确替代是使用 **`StringBuilder.reverse()`**(或 `StringBuffer.reverse()`)。我确定 `StringBuilder#reverse()` 存在;`String#reverse()` 不存在。
```java
public static String reverseString(String input) {
if (input == null) {
return null; // 如果你希望 null 抛异常,可改成 throw new IllegalArgumentException(...)
}
return new StringBuilder(input).reverse().toString();
}
```
说明:
- `new StringBuilder(input)` 创建可变的字符串构建器;
- `.reverse()` 会原地反转 `StringBuilder` 的内容;
- `.toString()` 再转回 `String`;
- 每次调用都新建 `StringBuilder`,所以单线程场景用 `StringBuilder` 即可,不需要用线程安全的 `StringBuffer`。
如果你要反转的不是普通 ASCII/UTF-16 字符,而是包含 emoji、组合字符等复杂 Unicode 文本,`StringBuilder.reverse()` 可能不满足“按字形/字素簇反转”的语义,需要额外处理。
问题三:它没看见你的代码,凭什么让它修 bug?
第二种”编”,不是编 API,是编现场。你只贴一段报错给它,它能给你讲得头头是道——但它其实什么都不知道。
我又做了组对照(同一段 NPE 日志):
只给日志,它能把异常机制讲得比多数人清楚(增强 for 循环会编译成 `items.iterator()`),但定位只能说”第 42 行大概率是类似这样的增强 for 循环”——猜的,因为它根本没看到代码。
给日志 + 代码 + 期望行为,它立刻换了副模样:定位到行(`for (Item item : order.getItems())`),给验证方法(先加临时日志打印 items 是否为 null,或写最小复现单测),还主动说”不要先改业务逻辑”。
只给报错它能当老师,给了代码它才能当同事。
这里有个小但被忽略的动作:告诉它你期望的行为是什么。”我期望正常结算并扣款成功”——现状和期望的差,才是 bug 的定义。你不说期望,它连”这算不算 bug”都得猜。
展开看实测原文(朴素 vs 工程化,沈剑2026-09-30实测:deepseek-flash,可复现,均为独立session以排除上下文影响)
实验材料(报错日志 + settle() 代码)
java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because "items" is null
at com.example.OrderService.settle(OrderService.java:42)
```java
public double settle(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
return total;
}
```
只给日志
线上报了这个错,帮我看看什么问题,怎么修?
java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because "items" is null
at com.example.OrderService.settle(OrderService.java:42)
只给日志的输出(机制科普,定位靠猜)
结论:**`items` 这个 List 是 `null`,不是空集合**。
第 42 行大概率是类似这样的增强 for 循环:
```java
for (OrderItem item : items) {
...
}
```
Java 编译增强 for 时,本质上会调用:
```java
items.iterator()
```
所以当 `items == null` 时,就会报:
```text
NullPointerException: Cannot invoke "java.util.List.iterator()" because "items" is null
```
空集合 `new ArrayList<>()` 或 `Collections.emptyList()` 是不会报这个错的。
---
## 常见原因
`items` 可能来自:
```java
List<OrderItem> items = order.getItems();
```
但 `getItems()` 返回了 `null`,例如:
1. 订单对象创建时没有设置明细
2. 数据库/ORM 映射没有查到子表数据,返回 null
3. 前端 JSON 没传 `items`,或传了 `"items": null`
4. Builder / Lombok 构造对象时漏设 items
5. 上游 service 传参就是 null
6. 缓存、RPC 返回对象字段缺失
---
## 紧急修复方式
如果业务上“没有明细也可以结算”,就转成空集合:
```java
List<OrderItem> items = order.getItems();
if (items == null) {
items = Collections.emptyList();
}
for (OrderItem item : items) {
...
}
```
如果业务上“没有明细不能结算”,应该尽早抛业务异常,而不是让它变成 NPE:
```java
List<OrderItem> items = order.getItems();
if (items == null || items.isEmpty()) {
throw new BizException("订单明细为空,无法结算");
}
for (OrderItem item : items) {
...
}
```
注意:`Collections.emptyList()` 是不可变集合,后面如果还要 `add`,请用:
```java
if (items == null) {
items = new ArrayList<>();
}
```
---
## 推荐根本修法
集合返回值尽量不要返回 `null`,统一返回空集合。
例如 Order 类里:
```java
public class Order {
private List<OrderItem> items = new ArrayList<>();
public List<OrderItem> getItems() {
return items == null ? Collections.emptyList() : items;
}
public void setItems(List<OrderItem> items) {
this.items = items == null ? new ArrayList<>() : items;
}
}
```
如果用了 Lombok Builder,要注意:
```java
@Builder.Default
private List<OrderItem> items = new ArrayList<>();
```
否则 builder 不传 items 时,可能还是 null。
---
## 建议排查点
在第 42 行前加日志:
```java
log.warn("settle items is null, orderId={}", orderId);
```
然后检查:
```java
List<OrderItem> items = order.getItems();
```
这个 `items` 是从哪里来的:
- `order.getItems()`?
- 数据库查询结果?
- 前端请求参数?
- RPC 返回?
- 缓存反序列化?
如果是前端或接口参数,建议加校验:
```java
if (items == null) {
throw new IllegalArgumentException("items must not be null");
}
```
---
## 一句话修法
在第 42 行前加:
```java
if (items == null) {
items = Collections.emptyList();
}
```
或者如果明细必须存在:
```java
if (items == null || items.isEmpty()) {
throw new BizException("订单明细不能为空");
}
```
不要把 NPE 当业务异常处理,最好从源头保证:**集合返回空集合,不返回 null;必须非空的参数入口校验。**
日志+代码+期望
线上报了这个错:
java.lang.NullPointerException: Cannot invoke "java.util.List.iterator()" because "items" is null
at com.example.OrderService.settle(OrderService.java:42)
这是相关代码:
```java
public double settle(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getCount();
}
if (order.getCoupon() != null) {
total = total - order.getCoupon().getAmount();
}
try {
paymentService.charge(order.getUserId(), total);
} catch (Exception e) {
log.info("settle failed");
}
return total;
}
```
我期望的行为是:正常结算并扣款成功。
请先定位最可能的原因(指到行),再给一个验证方法(怎么确认就是它),最后才给修复建议。别急着改代码。
日志+代码+期望的输出(定位到行+验证手册+先复现再改)
### 1. 最可能原因:第 42 行增强 for 的列表为 null
按你贴的代码,第 42 行应是对应:
```java
for (Item item : order.getItems()) {
```
异常信息是:
```text
Cannot invoke "java.util.List.iterator()" because "items" is null
```
这说明:**Java 在增强 for 开始时准备调用 `items.iterator()`,但 `items` 是 null**。
结合代码,最可能是:
- `order.getItems()` 返回了 `null`;
- 或者如果实际代码里写的是先赋值再循环,例如:
```java
List<Item> items = order.getItems();
for (Item item : items) {
```
那么就是第 42 行的 `items` 为 `null`。
可以排除几个方向:
- 不是 `order == null`,否则错误信息通常会提示 `order` 为 null;
- 不是 `item.getPrice()` 或 `item.getCount()`,因为还没进入循环体;
- `coupon` 也已经有判空,不是这里。
所以核心是:**订单的 items 集合为 null,而不是空集合。**
---
### 2. 验证方法:确认是不是 `order.getItems()` 返回 null
可以在第 42 行前加临时日志或断点,不要先改业务逻辑:
```java
log.info("order is null? {}", order == null);
if (order != null) {
log.info("orderId={}, items is null? {}", order.getId(), order.getItems() == null);
}
```
然后复现线上请求。如果日志显示:
```text
order is null? false
orderId=xxx, items is null? true
```
那就确认是 `order.getItems()` 返回 null。
也可以写一个最小单元测试:
```java
Order order = new Order();
order.setUserId(1L);
order.setItems(null); // 或者根本不设置 items
orderService.settle(order);
```
如果复现同样异常,并且堆栈指向第 42 行,就说明问题就是 items 为 null。
再检查这几个地方:
- `Order` 类里 `items` 字段是否默认初始化;
- 构造器、setter、数据库映射、JSON 反序列化、缓存反序列化是否可能产生 null items;
- 上游调用方是否可能传入一个没有 item 列表的订单对象。
---
### 3. 修复建议
先明确业务语义:**空 item 列表到底是否合法?**
如果业务上不允许空订单,建议在 `settle` 开头做明确校验,抛业务异常,而不是让 NPE 发生:
```java
if (order == null) {
throw new IllegalArgumentException("order is null");
}
if (order.getItems() == null || order.getItems().isEmpty()) {
throw new IllegalArgumentException("order items is empty, orderId=" + order.getId());
}
```
如果业务允许空订单,或者希望容错,则在遍历前归一化为空列表:
```java
List<Item> items = order.getItems() == null
? Collections.emptyList()
: order.getItems();
```
但要注意:这样 `total` 会变成 0,后面可能会执行 `paymentService.charge(order.getUserId(), 0)`,是否符合业务需要必须确认。
更根本的修复是在 `Order` 模型层面保证集合不为 null:
```java
private List<Item> items = new ArrayList<>();
```
并在 getter、setter、ORM/JSON 映射层做防空处理。比如 Jackson 可以配置空集合反序列化,JPA 要确认关联集合初始化或查询是否正确。
另外,当前这段:
```java
} catch (Exception e) {
log.info("settle failed");
}
```
会吞掉支付异常,线上排查会很难。虽然它不是本次 NPE 的原因,但建议至少改成 `log.error`,并考虑是否应该向上抛出或返回失败结果。
问题四:防瞎编的三件套是什么?
① 给出口:”不确定就说需要核实,不许猜”,让它可以说不;
② 要承诺:”把确定和不确定分开说”,让回答变成可核对的;
③ 给现场:代码、日志、期望行为、你的业务约束,你贴多少它才知道多少。
最后强调一次第 2 讲已经演示过的效果:拒答也是正确答案。信息不够的时候,它说”我不知道””我需要更多上下文”,比它硬写一个答案有价值得多。
实操演练
必做部分 + 一步加餐。准备一个你日常在用的 AI 助手。
检验一下
①你的防幻觉保险丝清单:给你前面几讲写的提示词加保险丝——”不许编+不确定就说+显式确定性+要验证方法”,挑一次实测(最好是你内部库/冷门 API 的场景),记录它是编了还是承认了。
这是资产库的第四格。
本讲实测来自沈剑的案例库 004、007(deepseek-flash,2026-09-23~09-30,独立会话,复现脚本和原始输出存档)。
(沈剑注:”不确定就说需要核实”这句话,我在人身上也用了十几年。一个工程师最值钱的品质之一,就是敢说”这块我不确定,我查一下”。AI 也一样。)
但还有一个没解决的问题:它会承认不确定了,也给足现场了——那让它干真活,写测试、改 bug,它干得怎么样?会不会又把”全绿”当讨好?——第 5 讲,我们讲把 AI 当同事用。
知其然,知其所以然。思路比结论更重要。
幻觉问题的权威长文:Lilian Weng · Extrinsic Hallucinations in LLMs——成因、检测、抵抗三段论,本讲的「边界与核实」是它的实战浓缩版。