AI系统开发思维
大家在谈论anthropic的时候,提到:对很多应用来说,优化单次LLM调用,在配合检索和上下文示例,通常就够了。 这个观点在实际落地的时候都要做哪些工作,怎么做
Claude responded: 这个观点的完整表述是:在你跳到multi-agent、复杂pipeline之前,先把单次调用做到极致。很多团队在单次调用还很粗糙的时候就去搭复杂系统,结果复杂度上去了,效果没好多少。
这个观点的完整表述是:在你跳到multi-agent、复杂pipeline之前,先把单次调用做到极致。很多团队在单次调用还很粗糙的时候就去搭复杂系统,结果复杂度上去了,效果没好多少。
让我把"优化单次LLM调用 + 检索 + 上下文示例"这三件事拆开说清楚。
一、优化单次LLM调用
这是最被低估的部分。大多数人的prompt停留在"描述一下任务"的水平,但一个认真优化过的prompt和随手写的prompt,效果差距可以非常大。
1.1 把任务分解写进prompt,而不是让模型自己分解
差的写法:
分析这份合同,告诉我有什么风险。
好的写法:
分析这份合同,按以下步骤:
1. 先识别合同类型和主要当事方
2. 列出所有涉及金额、期限、违约责任的条款
3. 对每个条款评估:对我方有利/不利/中性
4. 最后给出前三个最需要关注的风险点
每个风险点写明:条款位置、风险描述、建议处理方式。
区别在于:你把思考步骤显式化了,模型按步骤走,不容易跳步、漏项。
1.2 用输出格式约束来提高一致性
如果你的输出要被程序处理,格式约束是必须的:
输出格式严格按照以下JSON,不要有任何其他文字:
{
"risk_level": "high|medium|low",
"clauses": [
{
"clause_id": "string",
"content": "string",
"risk": "string",
"recommendation": "string"
}
],
"top_risks": ["string", "string", "string"]
}
1.3 给模型一个角色和判断框架
不是为了"让模型扮演角色"这种玩法,而是给模型一个明确的判断视角:
你是一个代表乙方的合同律师,你的目标是保护乙方利益。
在评估每个条款时,从乙方角度判断风险。
没有这个框架,模型会给出"中立"的分析,对你的具体场景用处有限。
1.4 系统提示和用户提示分开管理
系统提示放稳定不变的内容:角色定义、输出格式、判断框架、约束条件。 用户提示放每次变化的内容:具体输入、具体问题。
这样做的好处是:系统提示可以精心打磨一次,反复复用;用户提示保持简洁。很多人把所有东西都堆在用户提示里,每次都在重复解释背景,效率很低。
1.5 温度和采样参数
这个经常被忽视。对于需要准确、一致输出的任务(分类、提取、分析),temperature设低(0到0.3)。对于需要创意和多样性的任务(写作、头脑风暴),temperature设高(0.7到1.0)。用默认值处理所有任务是一种懒惰。
二、检索(RAG)
检索解决的问题是:模型不知道你的私有数据、最新信息、或者超出上下文窗口的长文档。
2.1 检索的完整流程
用户问题
↓
查询改写(Query Rewriting)
↓
向量检索 / 关键词检索 / 混合检索
↓
重排序(Reranking)
↓
上下文压缩(把检索到的内容精简)
↓
组装进prompt → LLM调用
↓
输出
每一步都有优化空间,但很多人只做了中间的"向量检索"这一步,前后都跳过了。
2.2 查询改写——被低估最多的步骤
用户的原始问题往往不适合直接用来检索。
原始问题:"上次说的那个方案怎么样了" 改写后:"项目方案进度更新 状态报告"
原始问题:"这个功能为什么这么慢" 改写后:"功能性能问题 响应时间慢 原因分析"
查询改写可以用一个小的LLM调用来做:
将以下用户问题改写为适合文档检索的搜索查询。
去掉指代词,补充可能的关键词,输出2-3个检索变体。
用户问题:{question}
这一步能显著提升检索召回率。
2.3 分块策略直接决定检索质量
文档怎么切块,对检索效果的影响比向量模型的选择更大。
按固定字数切(最常见但效果最差):切块不考虑语义边界,一句话可能被切成两半。
按语义边界切:按段落、按标题层级、按句子组切。同一个主题的内容在同一块里,检索时更容易命中。
父子切块:小块用来检索(粒度细,命中准),命中后返回它的父块(内容完整,上下文足)。这是目前实践中效果最好的策略之一。
文档结构:
├── 第3章:渠道管理(父块,2000字)
│ ├── 3.1 添加渠道(子块,300字)← 检索命中这里
│ ├── 3.2 渠道测试(子块,400字)
│ └── 3.3 禁用渠道(子块,250字)
返回给LLM的是:父块(第3章全文)
2.4 混合检索比纯向量检索更鲁棒
向量检索擅长语义相似("怎么发短信" 能找到"短信发送方法")。 关键词检索擅长精确匹配(产品型号、人名、代码片段)。
两者结合,用RRF(Reciprocal Rank Fusion)合并结果,覆盖两类查询。纯向量检索在精确名词查询上经常失败,这是实际落地中很常见的坑。
2.5 重排序(Reranking)
向量检索召回20条,不代表top 5就是最相关的。Reranker做的事是:用一个专门训练的模型,对查询和每个候选文档做精细的相关性打分,重新排序。
常用方案:
- Cohere Rerank API(直接可用)
- BGE-Reranker(开源,可本地部署)
- 用LLM做rerank(效果好但慢,适合低延迟要求不高的场景)
这一步通常能让最终结果的相关性提升20-30%,但很多系统跳过了。
2.6 检索结果的质量评估
很多人建完RAG系统,只用肉眼感受好不好。实际上应该建一个评估集:
评估集格式:
问题 → 正确答案应该来自哪个文档片段
评估指标:
- 召回率:正确文档是否在检索结果里(不管排第几)
- 精确率:top-k里有多少是真正相关的
- MRR:正确文档的排名
没有评估集,你不知道优化是否真的有效,改了半天可能在某些查询变好、另一些查询变差,整体没有进步。
三、上下文示例(Few-shot)
这是prompt engineering里最稳定有效的技术,但需要认真设计,不是随手放几个例子。
3.1 示例的作用
示例告诉模型:不只是任务是什么,而是我期望的输出风格、详细程度、判断标准是什么样的。
纯指令有时候描述不清楚的东西,一个好例子能立刻说清楚。
3.2 示例怎么选
不要用随手想出来的例子。好的示例来自真实数据:
- 先用zero-shot(不给示例)跑100个真实输入
- 人工标注哪些输出是好的、哪些是差的
- 从好的输出里选3-5个作为示例
- 从差的输出里分析错误模式,针对性地写示例来纠正
这样选出来的示例覆盖了模型真实容易犯错的地方,而不是你主观认为重要的地方。
3.3 动态示例比固定示例更好
固定示例:每次调用都用同样的3个例子。
动态示例:根据当前输入,从示例库里检索最相似的3个例子放进prompt。
python
# 示例库:500个已标注的输入输出对
# 用户来了一个新输入
# 从示例库里检索最相似的3个 → 放进prompt
示例库
↓ 向量检索(用输入做查询)
最相似的3个示例
↓
动态组装进prompt
动态示例的效果通常比固定示例好,因为示例和当前输入越相似,模型越能迁移。
3.4 示例的数量和顺序
数量:3-5个通常足够,不是越多越好。太多示例会压缩真实输入的上下文空间,而且模型会过拟合示例的风格。
顺序:最相关的示例放最后(离真实输入最近),因为模型对靠近输入的内容注意力更高。
三件事组合起来
单独做好每一件事是基础,但真正的效果来自把三者有机结合:
用户输入
↓
查询改写(小LLM调用)
↓
并行:
├── 检索相关文档(RAG)
└── 检索相似示例(Few-shot库)
↓
组装prompt:
系统提示(角色 + 框架 + 输出格式)
+ 动态示例(3个最相似的)
+ 检索到的相关文档
+ 用户输入
↓
单次LLM调用
↓
输出
这个架构没有agent、没有多步推理、没有工具调用,但如果每个环节都做扎实,能解决80%的实际业务问题,而且延迟低、成本可控、行为可预测。
什么时候这个架构不够用?
当任务需要真正的多步决策(下一步取决于上一步的结果)、需要调用外部工具、或者需要并行处理多个子任务时,才真正需要往agent方向走。但很多团队在还没把这一层做扎实之前就跳过去了,结果复杂度高、调试难、效果也没好多少。
你这里没有提到如何评估单次LLM调用是否最优,或者如何设计这个机制,类似agent一样,把自我优化当作他的目标之一,他能自己进步,这个需要怎么做
这个问题触到了一个真正有深度的工程问题。你说的本质是:让系统能观测自己的输出质量,并根据观测结果改进自己的prompt。这在工程上叫"prompt optimization loop"或者"self-improving pipeline"。
让我认真设计这个机制。
先想清楚"最优"是什么意思
在设计自优化之前,必须先回答一个问题:用什么信号判断一次调用是好还是坏?
这个信号来源有三类,决定了整个优化机制的设计:
信号来源
├── 显式反馈:用户直接告诉你好不好(点赞/踩、改写、投诉)
├── 隐式反馈:用户行为推断(采纳了输出?还是忽略了?复制了吗?)
└── 自动评估:另一个LLM或规则判断输出质量
三类信号各有局限:显式反馈稀少,隐式反馈有噪声,自动评估有偏差。好的系统需要把三类信号结合起来。
完整的自优化架构
┌─────────────────────────────┐
│ Prompt库 │
│ prompt_v1, v2, v3... │
│ 每个版本有性能记录 │
└──────────┬──────────────────┘
│ 当前最优版本
▼
用户输入 ──────────────→ 单次LLM调用 ──────────→ 输出
│ │
│ ▼
│ 评估层(Judge)
│ │
│ ┌───────┴────────┐
│ │ 评估结果+原因 │
│ └───────┬────────┘
│ │
▼ ▼
日志存储层 ←──────── 观测层
(输入/输出/评分) (聚合分析)
│
▼
优化触发器
(定期 or 质量下降时)
│
▼
Optimizer LLM
(分析失败案例 → 生成新prompt)
│
▼
候选prompt测试
(在历史数据上回测)
│
▼
A/B测试 or 直接替换
第一层:评估层(Judge)
这是整个系统的基础,没有评估就没有优化。
评估层的设计
用一个独立的LLM调用来评估主调用的输出质量,这叫"LLM-as-Judge"。
JUDGE_PROMPT = """
你是一个严格的输出质量评估者。
任务类型:{task_type}
用户输入:{user_input}
系统输出:{system_output}
参考标准:{criteria}
请从以下维度评估输出质量,每项1-5分:
1. 准确性:输出内容是否正确、无事实错误
2. 完整性:是否覆盖了输入中的所有要求
3. 格式合规:是否符合要求的输出格式
4. 简洁性:是否有冗余内容
对于每个低于4分的维度,必须写出:
- 具体问题是什么
- 一个改进的例子
输出严格按JSON:
{
"scores": {
"accuracy": int,
"completeness": int,
"format": int,
"conciseness": int
},
"overall": float,
"failures": [
{
"dimension": str,
"problem": str,
"example_fix": str
}
],
"pass": bool // overall >= 3.5 则为true
}
"""
关键设计原则:Judge必须给出具体的失败原因和改进例子,不只是打分。这些原因后面会喂给Optimizer。
评估标准怎么来
不同任务类型的评估标准不一样,需要分开定义:
CRITERIA = {
"extraction": """
- 所有要求提取的字段都存在
- 字段值与原文一致,无捏造
- 格式符合schema定义
""",
"classification": """
- 分类结果在允许的类别范围内
- 置信度与输出一致
- 边界情况有说明
""",
"generation": """
- 内容与输入主题相关
- 没有重复已知的错误模式
- 长度在要求范围内
"""
}
防止Judge偏差
Judge LLM本身也有偏差,主要问题:
- 位置偏差:倾向于给第一个或最后一个选项打高分
- 长度偏差:倾向于给更长的输出打高分
- 自我偏袒:同一个模型评估自己的输出
缓解方式:
# 对同一输出,用不同顺序评估两次,取平均
# 评估时明确提示"长度不是质量标准"
# 考虑用不同厂商的模型做Judge
# 对于关键任务,维护一个人工标注的golden set做校准
第二层:观测层
把所有调用的输入、输出、评估结果存下来,这是优化的原材料。
日志结构
{
"call_id": "uuid",
"timestamp": "ISO8601",
"prompt_version": "v3.2",
"input": {
"user_query": str,
"retrieved_docs": [str], # 如果有RAG
"examples_used": [str] # 如果有few-shot
},
"output": str,
"judge_result": {
"scores": {...},
"overall": float,
"failures": [...]
},
"user_signal": {
"adopted": bool, # 用户是否采纳了输出
"edited": bool, # 用户是否修改了输出
"explicit": int # 用户显式评分,-1/0/1
},
"latency_ms": int,
"token_count": int
}
聚合分析:发现系统性问题
单条日志看不出规律,需要聚合:
# 按输入类型分组,看哪类输入失败率最高
failure_by_input_type = logs.groupby('input_type')['pass'].mean()
# 看失败原因的分布
failure_reasons = logs[~logs['pass']].explode('failures')
top_failures = failure_reasons['dimension'].value_counts()
# 看prompt版本的性能曲线
version_performance = logs.groupby('prompt_version')['overall'].agg(['mean', 'std', 'count'])
# 找到具体的失败模式
# 把所有failures的problem字段聚类,找出反复出现的问题
这个分析的输出是给Optimizer的上下文,告诉它"系统在哪里系统性地失败"。
第三层:Optimizer
这是真正"自我改进"的部分。Optimizer是一个LLM调用,输入是失败案例分析,输出是改进后的prompt。
Optimizer的prompt设计
OPTIMIZER_PROMPT = """
你是一个prompt工程专家。你的任务是改进一个LLM系统的prompt,
使其在失败案例上表现更好,同时不破坏已经表现良好的场景。
## 当前prompt(版本{version})
{current_prompt}
## 性能统计
- 总调用次数:{total_calls}
- 通过率:{pass_rate}%
- 平均分:{avg_score}
## 系统性失败模式(出现超过5次的问题)
{failure_patterns}
## 典型失败案例(每种失败模式各3个)
{failure_examples}
## 已知的成功案例(不要破坏这些)
{success_examples}
## 任务
1. 分析失败的根本原因(不是表面现象)
2. 提出针对性的修改方案,每个修改说明:
- 修改了什么
- 为什么这个修改能解决对应的失败模式
- 可能带来的风险
3. 输出改进后的完整prompt
输出格式:
{
"root_cause_analysis": str,
"changes": [
{
"what": str,
"why": str,
"risk": str
}
],
"new_prompt": str
}
"""
关键约束:不要让Optimizer乱改
Optimizer如果没有约束,会倾向于生成越来越长、越来越复杂的prompt,堆砌大量示例,结果是过拟合失败案例、破坏原本工作良好的场景。
需要给Optimizer加约束:
OPTIMIZER_CONSTRAINTS = """
约束条件(必须遵守):
1. 新prompt的token数不得超过当前prompt的150%
2. 不得删除当前prompt中已有的格式约束
3. 修改必须针对失败模式,不得做无关的"优化"
4. 每次只解决最严重的1-2个失败模式,不要试图一次解决所有问题
"""
第四层:候选prompt验证
Optimizer生成的新prompt不能直接上线,必须先验证。
在历史数据上回测
def backtest_prompt(new_prompt, historical_logs, n=100):
# 从历史日志里抽样
# 包含:之前失败的案例(验证改进)+ 之前成功的案例(验证不退步)
test_cases = sample_balanced(
failures=historical_logs[~historical_logs['pass']],
successes=historical_logs[historical_logs['pass']],
n=n
)
results = []
for case in test_cases:
output = call_llm(new_prompt, case['input'])
score = judge(output, case['input'])
results.append({
'case_id': case['id'],
'was_failing': not case['pass'],
'new_score': score['overall'],
'old_score': case['overall']
})
# 关键指标
fixed_rate = mean(r['new_score'] > r['old_score']
for r in results if r['was_failing'])
regression_rate = mean(r['new_score'] < r['old_score'] - 0.5
for r in results if not r['was_failing'])
return {
'fixed_rate': fixed_rate, # 修复了多少失败案例
'regression_rate': regression_rate, # 破坏了多少成功案例
'deploy': fixed_rate > 0.3 and regression_rate < 0.1
}
上线标准:修复率 > 30%,且回归率 < 10%。这两个阈值需要根据你的业务容忍度调整。
小流量A/B测试
回测通过后,不是直接全量替换,而是先给10%的流量用新prompt:
def route_request(user_input, experiment_config):
if random() < experiment_config['new_prompt_ratio']: # 0.1
prompt = load_prompt('candidate')
version = 'candidate'
else:
prompt = load_prompt('current')
version = 'current'
output = call_llm(prompt, user_input)
log(version=version, input=user_input, output=output)
return output
# 跑够1000次后对比两个版本的pass_rate和user_signal
# 统计显著性检验确认差异真实存在
触发优化的时机
不是每天都跑优化,而是在以下条件触发:
def should_optimize(recent_logs, window=1000):
recent_pass_rate = recent_logs.tail(window)['pass'].mean()
baseline_pass_rate = load_baseline_pass_rate()
triggers = {
# 质量下降:近期通过率比基线低5个百分点
'quality_degradation': recent_pass_rate < baseline_pass_rate - 0.05,
# 定期优化:每积累500个新的失败案例
'accumulation': count_new_failures() > 500,
# 新的失败模式出现:某类错误突然增多
'new_failure_pattern': detect_new_patterns(recent_logs),
}
return any(triggers.values()), triggers
整个循环的实际样子
第1周:
- 部署系统,开始收集日志
- 人工标注200个golden cases,建立Judge校准基准
- 通过率:假设65%
第2周:
- 积累了800个失败案例
- 聚合分析发现:40%的失败是"格式不合规",30%是"遗漏字段"
- Optimizer针对这两个问题生成v2 prompt
- 回测:修复率42%,回归率6% → 通过
- A/B测试10%流量,1000次后通过率提升到71%
- 全量替换为v2
第4周:
- 新的失败模式出现:边界输入处理差
- 触发新一轮优化
- v3 prompt上线,通过率76%
第8周:
- 通过率稳定在78-80%
- 优化触发频率降低,系统趋于稳定
- 剩余的20%失败开始需要人工介入判断
(这部分可能是任务本身的模糊性,不是prompt问题)
一个重要的认知边界
这套机制能做到的是:在你定义的评估标准范围内,系统性地减少失败。
它做不到的是:
- 自动发现你的评估标准本身是错的
- 处理评估标准覆盖不到的新型失败
- 替代你对"什么是好输出"的根本判断
所以这套系统不是真正的"自主进化",而是"在人定义的目标函数下自动优化"。目标函数的定义和校准,始终需要人来做。这也是为什么Judge的golden set需要人工标注,而不能完全自动生成。
你设计的这套机制,和hermes claw的自进化机制相比,是同一个技术实现原理吗,都说hermes也能实现自进化和学习,它的设计和代码是如何实现它的自进化的。你的设计是否有可以借鉴它的地方。
我需要先搜索确认这个项目的真实情况,不想凭印象回答。现在我读到了足够的真实信息。可以做一个准确的对比分析了。
先说结论:两套机制的本质不同
表面上都叫"自进化",但Hermes和我设计的机制在进化的对象、进化的触发方式、信号来源上完全不同。不是同一个技术原理。
Hermes的自进化:三层机制叠加
Hermes的核心自进化体现在:持久记忆(agent自己策划事实写入MEMORY.md)、自主技能创建(复杂任务后自动创建可复用技能文档)、技能自我改进(使用中发现技能过时或不完整时自动修补)。
第一层:运行时技能学习(内置核心功能)
它的学习循环:从经验创建技能,在使用中改进技能,跨会话持久记忆,跨会话搜索历史对话,建立用户的深度模型。
机制是这样的:
用户执行一个复杂任务(5+个工具调用)
↓
任务完成后,agent自动反思:
"这个任务有没有可以复用的工作流程?"
↓
如果有 → 自动生成 SKILL.md 文件
写入 ~/.hermes/skills/ 目录
↓
下次遇到类似任务,agent先检索技能库
找到匹配的技能 → 直接复用
↓
使用过程中发现技能描述不准确/不完整
→ agent在当次会话中自动patch这个skill文件
关键点:这是经验→程序化知识的转化,技能文件是结构化的Markdown(SKILL.md格式),agent既是技能的消费者也是技能的生产者和维护者。整个循环发生在运行时,不需要离线训练步骤。
第二层:记忆层(跨会话持久化)
Hermes有插件式内存提供者——内置内存层可以通过已发布的memory-provider插件扩展,比如Honcho和其他后端。on_session_start钩子支持自动上下文加载。
记忆分三层:
- 事实记忆(MEMORY.md):agent自己判断什么值得长期记住,主动写入
- 会话记忆(SQLite FTS5):所有历史会话可被语义检索,存储可搜索的会话历史,并用LLM摘要处理
- 用户模型(Honcho dialectic):跨会话构建用户的深度画像,理解偏好、工作方式、判断标准
第三层:hermes-agent-self-evolution(研究级,独立repo)
使用DSPy + GEPA(Genetic-Pareto Prompt Evolution)自动进化Hermes Agent的技能文件、工具描述、系统提示和代码——通过反思性进化搜索产生可量化更好的版本。不需要GPU训练,完全通过API调用完成——变异文本、评估结果、选择最优变体。每次优化运行约2-10美元。
GEPA的工作流程:
读取当前skill/prompt/tool
↓
生成评估数据集(合成 or 真实session历史)
↓
GEPA优化器读取执行轨迹
理解"为什么失败"而不只是"失败了"
↓
提出针对性变异(不是随机变异)
↓
生成候选变体
↓
约束门控:
- pytest必须100%通过
- 大小限制(skill≤15KB)
- 语义保持(不能偏离原始目的)
↓
最优变体 → 提PR,人工review,不直接commit
关于防止自我欺骗的建议:将hermes-agent-self-evolution与定期回归检查配合使用,并用第二次评估来阻止糟糕的prompt变异。诀窍不是"进化更快",而是"进化时不悄悄变奇怪"。
两套机制的本质对比
| 维度 | 我设计的机制 | Hermes内置机制 | Hermes GEPA机制 |
|---|---|---|---|
| 进化对象 | Prompt文本 | 技能文件(SKILL.md)+ 记忆 | Prompt + 工具描述 + 代码 |
| 进化触发 | 质量下降/失败积累 | 任务完成后自动触发 | 人工/定期调度 |
| 信号来源 | LLM-as-Judge评分 | 任务复杂度(工具调用数) | 执行轨迹(execution traces) |
| 优化算法 | LLM直接生成改进建议 | 无算法,纯LLM判断 | GEPA遗传算法+Pareto选择 |
| 验证方式 | 历史数据回测+A/B | 使用中自然验证 | pytest+约束门控 |
| 人工参与 | A/B测试决策节点 | 基本无人工 | PR review必须通过 |
| 学习粒度 | 单个task的prompt | 可复用工作流程(跨任务) | 具体的skill/tool文档 |
| 持久化方式 | 版本化prompt库 | 文件系统(~/.hermes/) | Git PR |
Hermes有哪些地方值得借鉴到我设计的机制里
借鉴点一:执行轨迹比评分更有信息量
我的设计用Judge LLM打分,然后让Optimizer分析失败案例。Hermes GEPA的做法是直接读执行轨迹——agent做了哪些工具调用、中间推理步骤是什么、在哪一步出了问题。
GEPA读取执行轨迹来理解为什么失败(而不只是失败了),然后提出针对性改进。
这个信息比"Judge说输出质量3.2分"要丰富得多。改进方向:
# 现有:只记录输入输出和评分
log = {
"input": ...,
"output": ...,
"judge_score": 3.2,
"failures": [...]
}
# 借鉴Hermes:同时记录推理轨迹
log = {
"input": ...,
"reasoning_trace": [
{"step": 1, "thought": "...", "action": "...", "result": "..."},
{"step": 2, "thought": "...", "action": "...", "result": "..."},
],
"output": ...,
"judge_score": 3.2,
"failure_step": 2, # 在第几步出了问题
"failure_reason": "在步骤2错误地跳过了格式校验"
}
Optimizer拿到的输入从"输出结果不好"变成了"在步骤2的判断逻辑有问题",生成的改进建议会更精准。
借鉴点二:技能库概念——把优化结果结构化存储
我的设计把优化结果存为"prompt_v1, v2, v3",是线性版本。Hermes的技能库是按任务类型索引的知识库,不同任务类型有不同的技能,可以被检索和组合。
借鉴到我的设计里:
prompt_library/
├── extraction/
│ ├── contract-risk-extraction_v3.md ← 合同风险提取的最优prompt
│ ├── invoice-parsing_v2.md
│ └── performance_log.json ← 每个版本的历史性能
├── classification/
│ ├── intent-detection_v4.md
│ └── sentiment_v2.md
└── generation/
└── report-writing_v5.md
这样当一个新任务来了,先检索最相似的已有prompt作为起点,而不是每次从零开始优化,也不是用同一个通用prompt处理所有任务。
借鉴点三:GEPA的遗传算法思路——多样性+Pareto选择
我的Optimizer每次只生成一个候选prompt。GEPA的做法是同时生成多个变体(种群),然后用Pareto前沿选择:在多个评估维度上都不被其他变体支配的那些变体才进入下一轮。
这避免了一个常见问题:单一Optimizer生成的改进方案,容易在提升某个维度的同时降低另一个维度。
简化版实现:
def gepa_style_optimization(current_prompt, failure_cases, n_variants=5):
# 生成多个不同方向的改进变体
variants = []
mutation_directions = [
"重点修复格式不合规问题",
"重点修复遗漏字段问题",
"提高简洁性,减少冗余",
"增加边界情况的处理说明",
"重写整体结构但保持约束"
]
for direction in mutation_directions:
variant = optimizer_llm(current_prompt, failure_cases, direction)
variants.append(variant)
# 多维度评估每个变体
scores = []
for v in variants:
scores.append({
"variant": v,
"fix_rate": eval_on_failures(v, failure_cases),
"regression_rate": eval_on_successes(v, success_cases),
"token_efficiency": len(v) / len(current_prompt)
})
# Pareto筛选:找出没有被任何其他变体在所有维度上支配的变体
pareto_front = get_pareto_front(scores)
return pareto_front # 返回多个候选,人工或A/B决定最终选择
借鉴点四:约束门控在变体评估前就过滤
我的设计是先生成候选prompt,再回测,再A/B。Hermes在评估之前就有一道硬性约束过滤:
每个进化变体必须通过:完整测试套件(pytest 100%通过)、大小限制(技能≤15KB)、缓存兼容性(不能在对话中途改变)、语义保持(不能偏离原始目的)。
在我的设计里对应:
def validate_candidate_prompt(new_prompt, current_prompt):
gates = {
# 大小约束:不能超过当前的150%(防止无限膨胀)
"size": len(new_prompt) <= len(current_prompt) * 1.5,
# 格式约束:必须包含输出格式定义
"has_output_format": "输出格式" in new_prompt or "JSON" in new_prompt,
# 语义保持:核心任务描述不能消失
"semantic_preservation": check_core_intent_preserved(
current_prompt, new_prompt
),
# 最低质量门槛:在golden set上不能低于基线的80%
"baseline_floor": eval_on_golden_set(new_prompt) >= baseline * 0.8
}
# 任何一个门控失败,直接拒绝,不进入回测
if not all(gates.values()):
failed = [k for k, v in gates.items() if not v]
return False, f"约束门控失败:{failed}"
return True, "通过"
这样能在进入昂贵的回测之前过滤掉明显有问题的候选。
一个重要的认知校准
Hermes代表了从静态系统到进化实体的转变,优先适应性而非配置,优先学习而非脚本编写,优先简单性而非基础设施。
但有一点需要清醒认识:Hermes的"自进化"和我设计的"prompt优化循环",在进化对象上有根本区别。
Hermes进化的是技能文件和工作流程——它在学习"如何做事",这是程序化知识的积累。我的设计进化的是prompt文本——它在优化"如何表达任务",这是提示质量的改善。
两者都是有价值的,但解决的不是同一个问题。如果你的系统需要处理越来越多类型的任务,Hermes的技能库机制更合适。如果你的系统任务类型固定但需要持续提升单类任务的输出质量,我设计的优化循环更直接。
真正完整的自优化系统应该把两层都做:技能库解决"知道怎么做",prompt优化循环解决"每次都做好"。
所以,两者如果结合才是最佳的选择吗。你之前的设计解决单次和LLM沟通的问题,hermes优化解决问题过程中多次和人类以及LLM沟通的效率和准确性。如果是结合一起更好,怎么做到?
你的理解方向对,但我想先把这个区分说得更精确,再谈怎么结合。
你说的"单次LLM调用"和"多次沟通",本质上是两个不同层次的问题:
我的设计解决的:
同一个任务类型,每次调用怎么做得更准确
→ 优化的是"表达"——如何把任务说清楚
Hermes解决的:
跨任务、跨会话,积累可复用的工作方式
→ 优化的是"知道"——学会如何处理一类问题
两者结合不是简单叠加,而是需要设计清楚哪一层的信息往哪里流。
结合后的完整架构
用户输入
↓
┌─────────────────────────────────────────┐
│ 技能路由层(Hermes机制) │
│ 检索技能库 → 找到匹配的技能 │
│ 技能里包含:最优prompt版本 + 上下文示例 │
└──────────────┬──────────────────────────┘
↓ 技能存在 ↓ 技能不存在
┌──────────┴──────────┐ ┌────────┴────────────┐
│ 加载技能的prompt │ │ 使用通用prompt │
│ + 动态示例 │ │ 标记为"新任务类型" │
└──────────┬──────────┘ └────────┬────────────┘
└──────────┬────────────┘
↓
┌─────────────────────────────────────────┐
│ 单次LLM调用优化层(我的设计) │
│ RAG检索 + 动态few-shot + 格式约束 │
└──────────────┬──────────────────────────┘
↓
LLM调用
↓
┌─────────────────────────────────────────┐
│ 执行轨迹记录 │
│ input / reasoning steps / output │
│ judge评分 / 用户信号 │
└──────┬────────────────────┬─────────────┘
↓ ↓
┌──────┴──────┐ ┌────────┴────────────┐
│ prompt优化 │ │ 技能知识更新 │
│ 循环 │ │ 循环 │
│ (我的设计) │ │ (Hermes机制) │
└──────┬──────┘ └────────┬────────────┘
↓ ↓
更新prompt版本 更新/创建技能文件
└──────────┬─────────┘
↓
下次调用更好
关键点是中间那层:执行轨迹是两个优化循环共享的原材料,不是分开收集两份数据。
两个循环的触发条件和职责边界
这是结合时最容易搞混的地方,必须设计清楚。
prompt优化循环触发条件:同一任务类型,最近N次调用的Judge评分出现系统性问题,或者失败模式在积累。它只改prompt文本,不改技能文件的结构。
技能更新循环触发条件:任务完成后(尤其是复杂任务),发现执行路径里有可以封装的工作流程。或者技能在使用中被发现有错误/过时的内容。它改的是技能文件本身——包括但不限于其中存储的prompt。
一个具体例子:
技能文件(skill: contract-review)
├── 工作流程描述:先看甲乙方 → 再看金额条款 → 再看违约责任
├── 当前最优prompt:v4(由prompt优化循环维护)
├── 动态示例库:18个已标注案例
└── 已知边界情况:跨境合同需要特别关注管辖权条款
prompt优化循环负责:把v4升级到v5,改的是prompt文本
技能更新循环负责:发现跨境合同是个边界情况,
把这条知识写进技能文件的"已知边界情况"
信息流的具体设计
执行轨迹的结构
两个循环都需要的共享数据格式:
{
"call_id": "uuid",
"skill_id": "contract-review", # 用了哪个技能
"prompt_version": "v4",
# prompt优化循环需要的
"input": "...",
"output": "...",
"judge": {
"score": 3.8,
"failures": [{"dimension": "completeness", "problem": "..."}]
},
"user_signal": {"adopted": true, "edited": false},
# 技能更新循环需要的
"reasoning_trace": [
{"step": 1, "thought": "识别合同类型", "result": "NDA"},
{"step": 2, "thought": "扫描金额条款", "result": "未找到"},
{"step": 3, "thought": "这是NDA不涉及金额", "result": "跳过"}
],
"task_complexity": 7, # 工具调用次数
"novel_pattern_detected": true, # 这次任务走了不寻常的路径
"session_id": "...",
"timestamp": "..."
}
技能文件的完整结构
技能文件要同时服务于两个循环,所以结构要包含两层信息:
---
skill_id: contract-review
version: 1.3
created: 2026-01-10
last_updated: 2026-04-28
prompt_version: v4 ← 指向prompt优化循环维护的版本
total_uses: 847
avg_judge_score: 4.1
---
# 技能:合同风险审查
## 工作流程
(由技能更新循环维护——什么情况下走什么路径)
1. 识别合同类型(NDA / 采购合同 / 服务合同 / 劳动合同)
2. 按类型加载对应的审查重点
3. 扫描金额、期限、违约、管辖权四个核心维度
4. 生成分级风险报告
## 已知边界情况
(技能在使用中积累的特殊情况处理方式)
- 跨境合同:特别关注管辖权条款和适用法律
- 框架协议:不单独看金额,看累计额度上限
- 补充协议:必须关联主合同,单独看可能误判
## 当前prompt(由prompt优化循环维护)
→ 见 prompts/contract-review/v4.md
## 动态示例库
→ 见 examples/contract-review/(18个标注案例)
## 优化历史
| 版本 | 日期 | 平均分 | 主要改进 |
|------|------|--------|----------|
| v1 | 01-10 | 3.2 | 初始版本 |
| v2 | 02-15 | 3.6 | 修复格式不合规 |
| v3 | 03-20 | 3.9 | 补充边界情况描述 |
| v4 | 04-28 | 4.1 | 针对跨境合同优化 |
新任务类型的冷启动
一个全新任务类型出现时,技能库里没有对应技能,两个循环都还没有可用数据。这是结合架构里最容易有空档的地方。
处理方式:
def handle_new_task_type(user_input, task_type):
# 第一步:检索最相似的已有技能作为起点
similar_skills = skill_library.search(task_type, top_k=2)
if similar_skills:
# 基于相似技能的prompt做适配,不是从零开始
bootstrap_prompt = adapt_prompt(
base=similar_skills[0].prompt,
new_task_description=task_type
)
else:
bootstrap_prompt = DEFAULT_PROMPT
# 第二步:用通用prompt执行,但标记为"新任务类型"
result = call_llm(bootstrap_prompt, user_input)
log(task_type=task_type, is_new=True, ...)
# 第三步:降低技能创建门槛
# 新任务类型:3次成功调用后就可以创建技能
# (正常门槛是5次复杂任务)
if new_task_call_count(task_type) >= 3:
create_skill_from_traces(task_type)
一个反直觉的设计决策
两个循环的更新频率应该不同,而且技能更新循环应该比prompt优化循环慢。
prompt优化循环可以相对频繁——积累了200-300个失败案例就可以触发一次,因为它改的只是文本表达,影响范围有限,可以用A/B测试安全验证。
技能更新循环应该更保守——技能文件是系统的"长期记忆",改错了影响所有后续使用这个技能的调用。每次更新都要有强信号支持(多次会话证实,而不是一次偶发情况),而且更新完需要在历史数据上做全量回测,不是只看新数据。
OPTIMIZATION_THRESHOLDS = {
"prompt_update": {
"min_failures": 200, # 失败案例积累
"check_frequency": "daily",
"validation": "ab_test_100_calls"
},
"skill_update": {
"min_confirmations": 5, # 同一个新发现在多次会话中确认
"check_frequency": "weekly",
"validation": "full_backtest_500_calls",
"requires_human_review": True # 借鉴Hermes GEPA的PR机制
}
}
最后一条借鉴了Hermes GEPA的核心原则:所有技能变更通过PR,人工review,不直接commit。这个约束不是繁文缛节,而是防止系统悄悄进化出奇怪行为的关键安全阀——awesome-hermes的社区总结是"诀窍不是进化更快,而是进化时不悄悄变奇怪"。
