前言:为什么「请别改代码」不够
大模型以「续写更合理、更通顺的文本」为优化目标,没有编译器或运行时的硬约束。Notebook 润色场景里,它可能把 usr_id「规范」成 user_id,却没有同步改调用处关键字参数,学员一跑就 TypeError。在 System Prompt 里堆「禁止修改代码块」能降概率,无法从机制上消除;更糟的是,同类误伤还会出现在 CLI 参数、API 路径版本、示例 IP 等任何它认为「不够优雅」的地方。
因此工程上需要两类能力:
- 反思:生成后再审视——自我反馈或独立审查角色,减少「带着偏差自证正确」。
- 编排与规划:复杂目标拆阶段、拆角色,必要时用工具/执行环境给客观反馈,避免单 Agent 长链路上的注意力稀释与错误级联。
下面按 Spring AI ChatClient + ChatModel + @Tool / ToolCallback 与 Alibaba Graph StateGraph 给出可落地的 Java 思路;不在文中执行任意用户代码——外部执行须走沙箱、CI、或专用执行服务。
一、让 Agent 学会反思
1.1 事故复盘:变量名「规范化」与调用处不一致
# 原始(能跑)
def get_user_data(usr_id: str):
return f"Data for {usr_id}"
print(get_user_data(usr_id="u-123"))
# 被「润色」后(调用未改 → 运行失败)
def get_user_data(user_id: str):
return f"Data for {user_id}"
# print(get_user_data(usr_id="u-123")) → TypeError: unexpected keyword argument 'usr_id'
结论:只要润色与代码处于同一自由生成空间,模型就可能引入语义正确、工程错误的改动。
1.2 仅靠 Prompt 的边界
「绝对不要改代码块」属于负向列举,覆盖空间无限;模型仍可能把「改注释 / 改格式 / 改 import」与「改标识符」混在一起输出。可靠路径是:把「可改范围」收窄(例如只提交 Markdown 叙述块给模型),或 生成后再审 / 再跑验证。
1.3 反思的两种模式
1.3.1 模式一:自我反馈(Self Review)
要实现反思,最直接的思路是让模型自己审视输出结果。这里有两种实现方式,一种简单但效果有限,另一种更复杂但效果显著。以下是我整理的2种方法的总结:
(A)单步指令式:在同一 Prompt 里要求「润色 + 反思 + 再改」。实现便宜,但审查者与生成者同源,容易把误改当成「规范化」。
(B)两步「生成—审查」(推荐起步):两次独立 ChatClient 调用,第二次 System Prompt 固定为「严苛技术审查员」,只比对原文 vs 草稿,输出 通过 / 不通过 + 证据。与 Spring AI 天然契合:同一 ChatModel 或审查用更小模型皆可。

缺点:有明显的成本:每增加一个审查 Agent 就多一轮模型调用,token 消耗更高。
@Service
public class NotebookPolishReviewService {
private final ChatClient writer;
private final ChatClient reviewer;
public NotebookPolishReviewService(ChatModel chatModel) {
this.writer = ChatClient.create(chatModel);
this.reviewer = ChatClient.create(chatModel);
}
/** 第一步:只强调「叙述可改、代码块逐字保留」 */
public String polishDraft(String originalNotebook) {
return writer.prompt()
.system("""
你是课程作家。优化 Markdown/叙述的流畅度。
要求:所有 fenced 代码块(```...```)必须与输入逐字一致,不得改标识符、字符串、参数名、API 路径。
若无法做到,请原样返回代码块。
""")
.user(originalNotebook)
.call()
.content();
}
/** 第二步:独立审查视角 */
public String review(String originalNotebook, String draft) {
return reviewer.prompt()
.system("""
你是技术审查员。比对【原文】与【草稿】。
若仅叙述变化、代码块完全一致 → 第一行输出:通过
否则 → 第一行输出:不通过;随后列出每一处代码/配置被改动的证据。
""")
.user("""
【原文】
%s
【草稿】
%s
""".formatted(originalNotebook, draft))
.call()
.content();
}
}
审查员不要直接重写全文:把 review_result 喂回写作 ChatClient,由写作侧做有依据的修正,形成「生成—反馈」闭环;可循环直到 通过 或达到次数上限。
成本提示:长文档多轮重复送原文时,可结合 DashScope 上下文缓存(相同前缀在后续轮次降费,以阿里云当前计费文档为准),把「固定长原文」放在消息前缀稳定区,把「本轮任务」放在后缀变化区。
成本消耗大的解决方案之一:
在"生成-反馈"循环中,原始课程文档会在多次模型调用中重复传递——写作 Agent 需要它,审查 Agent 也需要它,修正阶段可能还要再用一次。如果文档很长(比如包含几十个代码示例的完整课程),这种重复会带来显著的 token 消耗。
阿里云百炼提供了上下文缓存(Context Cache)机制来解决这个问题:首次调用时缓存共享内容(如原始文档),后续调用检测到相同前缀就直接复用,缓存命中部分按标准单价的 20% 计费。
你可以这样组织 Prompt 结构:
[缓存部分 - 所有调用都相同]
## 原始课程文档
{很长的原始notebook内容}
[变化部分 - 每次调用不同]
## 任务
{写作指令 / 审查指令 / 修正指令}
## 待处理内容
{润色后的草稿 / 审查反馈等}
通过这种方式,原始文档的 token 在首次调用时按标准单价计费,在后续审查、修正环节则享受缓存折扣,从而显著降低多轮调用的总成本。
前面讲的都是自我反馈的实现方式,也是有缺陷跟使用局限的:
一般来说,自我反馈更适合静态文本层面的比对(如检查代码是否被意外修改),但当你需要验证代码是否能正常运行时,这种模式的效果比较差。
这是因为大模型擅长判断代码"看起来是否合理",但无法像编译器那样进行精确的语义验证。更关键的是,模型无法真正执行代码——它看不到运行日志、捕捉不到异常报错、也感知不到依赖库版本冲突或环境配置问题。
要解决这个问题,我们需要引入外部反馈机制。
1.3.2 模式二:外部反馈(External Feedback)
外部反馈的核心思路是:把大模型的生成结果放到真实环境中执行,再把执行结果(成功/失败、错误信息、测试报告等)反馈给模型,让它根据这些落地的事实进行迭代修正。
区别:
如果说自我反馈是“我认为我做得对不对”,那么外部反馈就是“事实证明我做得对不对”。
可以将我上一篇文章的学习工具使用 (Tool Use) 与反思机制结合起来。在这里,最直接的外部反馈来源,就是一个代码执行工具。AgentScope 内置了一个强大的 execute_python_code 工具,它为 Agent 提供了一个安全的代码解释器环境,使其能够真实地执行 Python 代码并获取结果或错误信息。
具体的工作流程如下:
生成:写作 Agent 润色文档,输出了包含代码的 notebook 内容。
与外部工具交互:系统自动提取出代码块,并调用 execute_java_code 工具尝试执行它。
获取外部反馈:工具返回执行结果,假设遇到了错误:TypeError: get_user_data() got an unexpected keyword argument 'usr_id'。
修正:Agent 接收到这份错误反馈,它现在明确地知道代码出现了什么问题。基于这份反馈,它会生成修正后的代码。

如何选择这两种反馈模式呢?这取决于你的具体需求、预算和可接受的错误率。比如,如果只是润色一篇没有代码的博客文章,那么可能根本不需要反思。但如果是修改一份包含几十个代码示例的交互式课程,那么引入一个基于代码解释器的“外部反馈”循环,就是确保文档质量、避免发布事故的必要投资。