提示词工程:怎么让 AI 别瞎编?(4/7)

给”可以说不”的出口、要可核对的承诺、给足现场

提示词工程第 4 讲封面:怎么让它别瞎编?

给”可以说不”的出口、要可核对的承诺、给足现场

做技术的人多少都被 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(可以是你们内部库的方法名)。先直接让它写代码;开新会话,加上”如果不存在先指出,不确定就说需要核实”再问一次。对比两次它怎么处理。
注意观察:它是直接编,还是承认不确定。内部库这种它不可能知道的领域,才是保险丝真正的试金石
(加餐·选做)同一个报错,只给日志 vs 给足现场
拿一个你真实的报错,先只贴日志问它怎么修;开新会话,补上代码和”我期望的行为是……”再问一次。对比定位的精确度。
第二次它应该能指到行、给验证方法,而不是列一堆”可能原因”

检验一下

检索练习 · 幻觉高发区
实测发现 String、Guava 的错误 API 都被纠正了。真正需要重点防幻觉的场景是?

检索练习 · 不确定就说
“不确定就说需要核实,不许猜”这条提示词的核心作用是?

检索练习 · 可核对的承诺
“我确定 StringBuilder#reverse() 存在”这句话的价值在于?

检索练习 · 上下文现场
修 bug 时,为什么”给日志+代码+期望”比”只给日志”强得多?

课后作业一份产出,自己留存(第 5 讲直接用):

①你的防幻觉保险丝清单:给你前面几讲写的提示词加保险丝——”不许编+不确定就说+显式确定性+要验证方法”,挑一次实测(最好是你内部库/冷门 API 的场景),记录它是编了还是承认了。

这是资产库的第四格。

作业实现思路你刚加的保险丝,在我的案例库里对应两处:案例 004(”不确定就说需要核实”+显式确定性”我确定 X 存在”)和案例 007(给足现场:日志+代码+期望行为)。先自己动手做,再对照着看。提醒一句:我实测 String/Guava 它都纠正了,你的内部库场景结果可能完全不同——那才是你的主场。案例库完整内容,后续我会分享出来。
本讲一手来源

本讲实测来自沈剑的案例库 004、007(deepseek-flash,2026-09-23~09-30,独立会话,复现脚本和原始输出存档)。

(沈剑注:”不确定就说需要核实”这句话,我在人身上也用了十几年。一个工程师最值钱的品质之一,就是敢说”这块我不确定,我查一下”。AI 也一样。)

但还有一个没解决的问题:它会承认不确定了,也给足现场了——那让它干真活,写测试、改 bug,它干得怎么样?会不会又把”全绿”当讨好?——第 5 讲,我们讲把 AI 当同事用。

知其然,知其所以然。思路比结论更重要。

延伸阅读

幻觉问题的权威长文:Lilian Weng · Extrinsic Hallucinations in LLMs——成因、检测、抵抗三段论,本讲的「边界与核实」是它的实战浓缩版。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

京ICP备2025137728号-3