福生无量摸鱼天尊

agent系列(五):agent架构的落地思考

2026/04/22
37
0

最近在楼下停车场散步,反思了一下最近遇到的bug,觉得蛮有意思的,写篇blog分享一下。

prompt engineering的useful和useless

上文我们已经从架构的方面去讨论过多种的agent架构,主要还是分为隐式plan、半显式plan和显式plan的三种不同的agent风格,

那么在前年的prompt engineering的时代,RAG是那时候的热门话题,为什么要RAG呢,是因为LLM落地的第一大问题:大模型训练的语料是公开语料和通用规律,它是不知道公司内部文档、你自己的笔记和古早之前的上下文的,那时候连web search的清洗都是非常困难的,所以大模型的幻觉非常强。

RAG作为一个能力层,要做的就是embedding、chunked、检索召回、rerank这种非常常用的workflow,这些都是func call的tools use,所以那个时候已经开始修工具了,那时候面临的最大的两个问题,一个是召回的时候搜不准,第二个就是组织context给LLM组织的不好

很多人刚做 RAG 时,第一反应是把 top_k 拉大:k=5 不够就上 k=20,dense 不够就 hybrid,hybrid 不够就再堆 reranker。结果常见现象不是答案更稳,而是上下文更多了,回答反而更飘了。这两年,尤其是 2024 和 2025,RAG 检索侧一个非常明确的结论慢慢成形:问题不只是 recall 不够,而是 retrieved context 里混进了太多“看起来相关、其实会害人”的东西。

2024 年不少工作开始正面这个问题,比如从 chunk 粒度、query rewriting、树/图结构检索、long-context retrieval、reranking 一路拆。很多 RAG 系统默认把用户原问题直接拿去检索,但真实 query 常常是带省略、带指代、带上下文依赖、带任务意图混杂的。

那时候就提出了RAG 里 query rewrite 不是锦上添花,相关工作还提出用 ranking feedback 来改写 query,让改写不是只看“像不像自然语言问题”,而是看“是否更能检到好文档”。最朴素但非常有效的代码是把检索目标显式写进 prompt:

def rewrite_query(llm, user_query, chat_history=None):
    prompt = f"""
You are rewriting a query for retrieval, not for answering.

Goal:
1. Remove vague references.
2. Expand missing key entities.
3. Preserve factual constraints.
4. Generate a search-oriented query, not a conversational reply.

User query:
{user_query}

Return JSON:
{{
  "search_query": "...",
  "must_have": ["..."],
  "must_not_confuse_with": ["..."]
}}
"""
    return llm(prompt)

这类改法的关键不在“变长”,而在把 retrieval objective 写出来。否则改写器很容易把 query 改得更流畅,但更不利于召回。这个思路和 ranking-feedback 方向是一致的:query rewrite 应该对检索结果负责,而不是只对语言表面负责。2025 年这条线又进一步发展成了带多方面反馈的 MaFeRw:先取 top-K,再让 reward/reranker 信号反哺 query rewriter。

另一条 2024 年很有代表性的路线是 LongRAG。它直截了当地指出,传统 RAG 往往让 retriever 在海量短 chunk 里找“针”,retriever 很重,reader 却很轻,设计不平衡。LongRAG 的做法是把 Wikipedia 处理成 4K-token 的 retrieval unit,显著减少检索单元总数,取得挺好的结果。

这背后的直觉非常实用:你不是一定要把 chunk 切得更小,也可以反过来,把“召回单元”做大,把“证据抽取”放到后面。

# 传统
retrieval_units = split_into_256_token_chunks(docs)

# LongRAG 风格
retrieval_units = build_4k_semantic_units(docs)  # 相关页面/段落打包成大单元
top_units = retriever.search(query, k=2)

# 再从大单元里做二次证据提取
evidence = extract_supporting_spans(top_units, query)

这在落地上非常有帮助,因为如果你的场景是论文、技术文档、长报告、PRD,这种思路往往比死磕 256-token chunk 更靠谱,因为很多问题的证据本来就跨段落、跨小节。LongRAG 的价值不只是长,而是它逼着系统设计者重新想:召回阶段到底要返回“答案片段”,还是返回高质量证据,这里就足以体现一种工程和内容上的割裂感,召回到的内容跟回答的准确内容之间的不匹配性

在内容难以解决的时候,结构的优化搬上来了,RAPTOR 是 2024 年很值得记住的一篇,因为它不再把语料库看成一堆平铺 chunk,而是递归摘要形成树结构,再在树上做 retrieval。论文报告在 QuALITY 上相对提升最高可达 20% 的绝对准确率。这非常重要,因为它解决的是多文档跨段落的内容整合问题,这是平铺 chunk没法解决的,至此,开始出现了垂类的专精设计,qa的chat系统这种局部事实问答的用平铺 chunk + 简单召回也能做的不错,而多模态文档问答,就必须要给系统一个能表达全局结构的检索对象,这也是 RAPTOR 和 GraphRAG 在 2024 年特别有启发性的地方。到 2025 年,研究已经进一步把问题往真实系统推进,开始讨论长上下文里 hard negatives 的破坏、测试时 compute 怎么分配、OCR 噪声如何级联污染整个 retrieval → generation 链路。

既然检索没法真正的确定和最终结果语义上的正确性,有相关工作就开始优化rerank了,我既然没法确定,那我就都搜罗过来,然后优化rerank不就好了嘛,这就有了rerankRAG。

RankRAG 很有意思,因为它直接指出一个现实问题:LLM 并不擅长阅读太多 context,而 dense embedding retrieval 也不一定能把最相关 chunk 放到最前面,所以单独靠 retriever 输出 top-k 还不够。RankRAG 提出用同一个指令调优的LLM同时做 context ranking 和 answer generation,它主要是被统一训练成两种能力,然后在推理时分两步调用:先 rerank,再 answer。这跟它的训练数据有关,简单来说就是构造数据集让ranking 和 generation 互相增强,这样做能让LLM回答问题需要学会识别哪些 context 真有用,而识别相关 context 又会反过来提升回答质量。

最小可落地版本甚至不需要训练 RankRAG 本体,先做 cross-encoder rerank 就已经能吃到大部分收益:

cands = hybrid_search(query, top_k=50)

pairs = [(query, d["text"]) for d in cands]
scores = cross_encoder.predict(pairs)

reranked = [doc for _, doc in sorted(zip(scores, cands), reverse=True)]
contexts = reranked[:6]

这里真正关键的是:第一阶段追求高召回,第二阶段追求高精排
不要指望一个单向量 ANN 检索同时把这两件事全做了。RankRAG 这类 2024 工作,本质上是在告诉大家:RAG 的排序目标需要更接近最终 answerability,而不是只接近 embedding similarity。

到了 2025 年,一个很明显的趋势是:大家开始系统性地研究 hard negatives、长上下文下的噪声扩散、测试时算力分配、文档解析误差 对 RAG 的伤害。也就是说,研究重点慢慢从“召回模块本身”转向“整个 retrieval pipeline 的鲁棒性”,这也是我觉得agent infra这个东西的开始,因为一旦涉及到E2E的优化,避免不了的就是各种pipeline的build和eval。

ICLR 2025 的 Long-Context LLMs Meet RAG 很值得看一遍。它明确说:随着 retrieved passages 数量增加,很多 long-context LLM 的输出质量会先上升,然后下降;论文把重要原因归结为 retrieved hard negatives 的破坏,如果我能吹这篇文章,我愿称之为context engineering的开篇鼻祖,我当初就是看到它才意识到context的重要性,这也基本上否决了scaling rerank-general的可能性,让它变成了一种数据处理手段。

这会把你的 pipeline 从:

docs = retriever.search(query, k=20)
answer = llm(query, docs)

改成:

docs = retriever.search(query, k=20)
docs = rerank(docs, query)[:8]
docs = filter_irrelevant_or_redundant(docs, query)
answer = llm(query, docs)

这个小改动的结论就是:RAG 的精度问题,常常不是 retriever 抓不到,而是 generator 没法在噪声环境里稳定读对。而这个小改动的文章提出一个很关键的视角:RAG 的 test-time compute 不只是“多拿几篇文档”,还包括 in-context learning、iterative prompting 等策略组合。论文报告在最优分配下,benchmark 上最高能比标准 RAG 带来 58.9% 的提升。这意味着一个成熟的 RAG 系统不应该只暴露 top_k 这一个旋钮,在多轮CoT中,每一轮都要控制token的数量,只取最有效的token,token管理很重要,所以工程上又要加码了:

def answer(query):
    raw_docs = hybrid_search(query, top_k=40)          # recall budget
    ranked_docs = rerank(query, raw_docs)[:10]         # ranking budget
    support = select_diverse_supports(query, ranked_docs, max_tokens=6000)

    draft = llm_reason(query, support, mode="draft")   # reasoning budget
    if low_confidence(draft):
        refined_q = rewrite_from_failure(query, draft, support)
        extra_docs = hybrid_search(refined_q, top_k=20)
        support = merge_and_redo(support, extra_docs)

    return llm_reason(query, support, mode="final")

这就完了吗!其实没完,为什么呢,多模态平台中,效果不佳可能不是召回或者rerank,而是ocr。这时候你可能会理解为什么deepseek ocr能这么火热(当然deepseek本身流量也大),后来各种ocr都开始刷榜了。

在功能稳定之后,我们不妨来聊聊架构,其实你发现了,我们长篇大论都在讨论rag,我由始到终都没提到过agent loop,为什么呢,因为在prompt engineering的时代,agent loop并不在重要,当时的微调手段比较单一,可用数据也不多,更重要的是刚开始投入并不大也没那么多卡,所以业界普遍都在做RAG,提升效果明显,所以这个时期,会出现workflow在RAGService中运行的情况:

# agentservice.py
class AgentService:
  async def create()   # 仅仅是作为LLM和agent的装配站
  async def list_all()
  async def get() 
  async def delete()
  async def get_runs()

# ragservice.py
async def agentic_rag(prompt_context, agent, memory):
  model = agent.get_model()
  memory_context=memory._format_memories(memories)

  messages=self._build_messages(prompt_context, memory_context)
  
  for turn in range(max_turns):
    LLMResult = self._llm.chat(model, messages)
    answer = LLMResult.content
    tools = LLMResult.tool_calls
    
    if not result.tool_calls:
      answer = result.content # 判断是否是最终答案

    tool_log = [self._run_tool(tc, exec_map) for tc in result.tool_calls] # 不是答案就一定要用tools
    messages.append(tool_log)

  final: LLMResult = await self._llm.chat(
    model, messages, temperature=temperature, web_search_enabled=web_search_on,
  )
  answer = final.content or "无法在限定轮次内完成回答。"

这就很有意思了,agent loop居然不在agentservice中,而是在ragservice中,而且即便要装配skills、web search这种tools,写进agentic_rag()里也不会违和,这就很有意思了,我们抛开编码习惯和可能是之前写文件名没改之类的可能性来说,这是不是表达了一种agent可以被包在rag里的感觉,而这种感觉,我愿称之为纯正的prompt engineering,在这个体系里,rag是可以cover agent的

所有在这个体系内修修改改的,都是prompt engineering相关的工作,因为本质上都是在修改给LLM的数据,就好比喂给客人的到底是龙虾鲍鱼还是白菜豆腐,原材料和厨师都至关重要。通过对tools的升级和限制,放大LLM的长处,让system更加的useful,减少LLM的错误,让system少一点useless,这就是我理解的prompt engineering。

真正的考虑context engineering

harness真的只是用于自进化吗