用工程化思维把 LLM 从“聪明的孤岛”变成可靠的产品 —— 基于 Weaviate《Context Engineering》读后实践指南

85 阅读13分钟

一、核心挑战与原则(为什么要做 Context Engineering)

1. 上下文窗口的根本限制

  • LLM 的「工作内存」是有限的(context window),把太多东西塞进去会导致:

    • 分心(Context Distraction) :历史信息过多,模型过度依赖历史行为,丧失即时推理质量。

    • 混乱(Context Confusion) :无关文档或工具描述干扰工具选择或答案生成。

    • 冲突(Context Clash) :不同来源互相矛盾,模型不知道取谁。

    • 污染(Context Poisoning) :错误信息被保存并被反复利用,错误放大。

原则结论:不是盲目追求更大上下文窗,而是做“上下文卫生”:

  • 验证(Quality Validation)

  • 总结/压缩(Summarization / Compression)

  • 剪枝(Pruning)

  • 外部卸载(Offload to DB / vector store)

2. 系统架构(动态流水线)

书中强调:不要把系统设计成固定的 “retrieve → generate”,而要把它做成一个 动态决策循环

用户输入 → 查询增强 → 检索(RAG)→ 提示(Prompt)→ 智能体协调(Agent)→ 工具行动(Tool call)→ 记忆更新 → 输出

智能体会在各环节做判断:是否需要检索、是否需要重写查询、是否选择工具、何时压缩历史等。


二、关键组件精要(以及在工程中的实操建议)

下面每一部分都给出“为什么重要”“工程实践要点”“示例/伪码或注意事项”。


1. 智能体(Agent)——系统的大脑

为什么重要:负责把静态步骤变成动态策略(是否重试检索、是否切换知识库、是否链式调用工具)。

工程实践要点

  • 把 agent 设计为决策层而非直接执行器:它输出“计划”(plan)与“动作(要调用哪个工具、参数)”,由执行层负责调用和安全检查。

  • 区分单体 agent(适用于中等复杂任务)与多体架构(specialized agents):

    • 单体:实现简单,内部负责多任务决策。
    • 多体:每个 agent 专注一类任务(query agent、memory agent、tool agent),需要 orchestration/arbiter。
  • 添加反脆弱措施:当工具返回错误或检索结果不相关时,agent 应能 重写查询 / 切换策略 / 降级回答

示例:Agent 输出计划的伪 JSON

{
  "plan": [
    {"step": "rewrite_query", "args": {"strategy": "add_keywords"}},
    {"step": "search_vector_db", "args": {"collection": "product_docs", "k": 5}},
    {"step": "call_tool", "args": {"name": "price_checker", "params": {"sku": "123"}}}
  ],
  "thoughts": "initial query is vague; will expand and search product_docs first"
}

2. 查询增强(Query Augmentation)——把模糊请求变成可检索的查询

关键技术

  • 重写(Rewriting) :清理噪音、补充领域关键词、恢复省略的上下文。

  • 扩展(Expansion) :生成多个查询变体以覆盖不同表达(注意避免过度扩展导致 drift)。

  • 分解(Decomposition) :把复杂问题拆成子问题并行检索/处理。

  • Query Agent:高级版本,用 agent 动态决定查询策略、路由多个集合、迭代改进查询。

工程建议

  • 用 LLM 做 rewrite/expansion,但限制每次扩展的数量(例如最多 3 个变体)。

  • 对扩展结果进行 rerank(可用 LLM 对检索结果做再排序)以控制噪声。

  • 对于复杂任务,优先做 decomposition 并在合成阶段把子答案拼接并做一致性验证。

伪码示例(Node.js 风格)

async function augmentQuery(rawQuery) {
  const rewritten = await callLLMRewrite(rawQuery);
  const expansions = await callLLMExpand(rewritten); // returns up to N variants
  return [rewritten, ...expansions];
}

3. 检索(Retrieval)——把外部知识精确带入模型

核心:Chunking(分块)设计决定检索质量。

分块策略概览

  • Fixed-size:简单、快速,但可能截断语义。

  • Recursive(递归) :按段落→句子→词级别优先分割,保结构。

  • Document-based:按文档语义/标记(Markdown headings / HTML tags)。

  • Semantic / LLM-based:用 LLM 自动判断语义界限,生成语义完整的 chunk。

  • Late-chunking:先用长上下文模型嵌入整文,再基于 token-level 做 chunk(保语境)。

Pre vs Post chunking

  • Pre-chunking:离线处理,查询时快速;缺点是策略固定,改变需重建索引。

  • Post-chunking(Chunk on demand) :检索整文后根据查询动态切分,灵活但有延迟。

工程实践

  • 对于常变内容/长文档建议采用 后分块layered (hierarchical) chunk + 预计算摘要。

  • 总是在 chunk 中保存足够的来源元数据(doc id、章节、位置信息)用于溯源和来源显示。

  • 对检索返回结果做 二次 rerank(用 LLM 打分),再把最相关的 2-4 个 chunk 注入 prompt。

JS 分块伪实现(递归)

function recursiveChunk(text, maxTokens, separators = ['\n\n', '\n', '. ']) {
  // 如果 text token 数 <= maxTokens 返回 [text]
  // 否则按第一个合适的 separator 切分,然后对每个片段递归调用,保证不超限并保留上下文
}

4. 提示技术(Prompting)——把检索到的信息引导进准确结论

经典技巧

  • Chain of Thought (CoT) :要求模型按步骤推理(在需要复杂推理时很有效)。

  • Few-shot:提供示例以引导输出格式和风格。

  • ReAct:交替输出“思考(thought)”与“动作(action)”,适合需要工具交互的场景。

工具提示(Tool Prompting)要点

  • 明确 函数名、参数类型与含义、返回值结构、限制(例如时区、地域限制)。

  • 给出 few-shot 示例说明何时使用该工具、如何填参数、期望结果格式。

示例工具描述片段(system prompt 用)****

get_weather(city: string, date: string) — returns {high, low, condition}. Use only for user requests that ask current or forecast weather; do not call for historical queries.


5. 记忆(Memory)——让 agent 有“历史感”

记忆类型

  • 短期(Short-term / working memory) :存在 context window,用于当前任务。

  • 长期(Long-term) :存在 vector DB 等外部存储,用于跨会话的偏好/事实调用(episodic/semantic/procedural)。

管理原则

  • 选择性存储:不是什么都存。用重要性评分(LLM 自动评估)作为入库门槛。

  • 定期修剪:按时间 & 访问频率 & 可信度清理旧记忆(例如超过 90 天且不常用的记忆只保留摘要)。

  • 任务定制检索:不同任务路由到不同记忆集合(support tickets → episodic;product specs → semantic)。

实操建议

  • 写一个 shouldSave(memory) 的决策函数,基于关键词、重要性分数、上下文角色等。
  • 保存的记忆分为“完整记录(稀有)”与“摘要(常态)”,摘要更便于检索与避免污染。

6. 工具(Tools)——让 agent 能“做事”

从欺骗输出到函数/工具调用:早期靠 prompt 生成命令文本,现行做法是 函数调用(structured function call) 。关键在于工具描述与安全边界。

工程实践

  • 工具 metadata 非常重要:name, inputs (types + formats), outputs, limitations。把它们放入 system prompt 或 agent 的工具目录中。

  • 执行层在调用前做 参数验证 & 权限检查(防止 LLM 注入恶意参数)。

  • 设计工具时考虑“可回滚/幂等性” —— 如果动作失败,如何补偿或重试。

  • 跟踪每次工具调用的观测(observation),并将结果 feed 回 agent 作为思考输入(Thought-Action-Observation 循环)。

未来方向:MCP(Model Context Protocol)等标准化协议会简化工具间集成——用标准接口替代海量定制代码。


三、端到端工程样板(Orchestration & Implementation Tips)

下面给出一个实用的 orchestration loop(伪代码)和各环节的工程注意事项。以 Node.js / TypeScript 风格说明。

async function handleUserRequest(userId, rawQuery) {
  // 1. Query augmentation
  const queries = await augmentQuery(rawQuery); // rewrite + expansions

  // 2. Agent analysis: plan what to do
  const plan = await agentPlan(userId, queries); // may output steps

  // 3. Execute plan step-by-step
  for (const step of plan) {
    if (step.type === 'search') {
      const docs = await vectorSearch(step.collection, step.query, {k: step.k});
      const reranked = await rerankWithLLM(docs, rawQuery);
      step.result = reranked;
    }
    if (step.type === 'call_tool') {
      // validate params
      validate(step.args, toolSchema[step.toolName]);
      const res = await callTool(step.toolName, step.args);
      step.result = res;
    }
    // Agent inspects observations and may alter plan (self-healing)
    await agentReflect(step, step.result);
  }

  // 4. Synthesis: use LLM to compose final answer using retrieved chunks + tool outputs
  const finalAnswer = await generateAnswer({
    promptTemplate,
    retrieved: collectTopChunks(plan),
    toolsOutput: collectToolResults(plan),
    history: getWorkingMemory(userId)
  });

  // 5. Memory update: decide whether to store summary
  if (shouldSaveMemory(rawQuery, finalAnswer)) {
    saveMemory(userId, summarize(finalAnswer));
  }

  return finalAnswer;
}


/**
 * Orchestration Skeleton - TypeScript
 *
 * Single-file project skeleton that demonstrates an orchestration loop for
 * a RAG + Agent system. Includes stubs for:
 *  - QueryAugmentService
 *  - VectorSearchAdapter (in-memory stub)
 *  - AgentPlanner (planner that outputs steps)
 *  - ToolExecutor (structured tool call + validation + auditing)
 *  - MemoryStore (short + long term memory stubs)
 *
 * Usage:
 *  - import { Orchestrator } from './orchestration-skeleton'
 *  - call orchestrator.handleUserRequest(userId, rawQuery)
 *
 * This file is intentionally framework-agnostic and contains synchronous
 * and asynchronous placeholders that you'll replace with real implementations
 * (embedding model, vector DB client, LLM call, auth, telemetry, etc.).
 *
 * Design goals:
 *  - Clear interfaces for each component
 *  - Small, testable pieces for early integration tests
 *  - Instrumentation hooks (events/logging)
 */

// ---------------------------
// Types
// ---------------------------

export type UserId = string;

export type Query = string;

export type Chunk = {
  id: string;
  text: string;
  docId?: string;
  meta?: Record<string, any>;
  score?: number; // optional re-rank score
};

export type ToolCall = {
  name: string;
  args: Record<string, any>;
};

export type ToolResult = {
  success: boolean;
  output?: any;
  error?: string;
};

export type PlanStep =
  | { type: 'rewrite'; payload?: any }
  | { type: 'search'; collection: string; query: string; k?: number }
  | { type: 'call_tool'; tool: ToolCall }
  | { type: 'synthesize' }
  | { type: 'save_memory'; payload?: any };

export type Plan = { steps: PlanStep[]; thoughts?: string };

// ---------------------------
// Utilities / Simple Logger
// ---------------------------

export const nowIso = () => new Date().toISOString();

export const log = (...args: any[]) => {
  // replace with structured logger in prod
  console.log('[orchestration]', nowIso(), ...args);
};

// ---------------------------
// Query Augmentation Service
// ---------------------------

export class QueryAugmentService {
  constructor(private opts?: { maxExpansions?: number }) {}

  async rewrite(raw: Query): Promise<string> {
    // TODO: replace with LLM rewrite call
    log('rewrite.raw', raw);
    const cleaned = raw.trim();
    // simple heuristic: remove multiple spaces
    return cleaned.replace(/\s+/g, ' ');
  }

  async expand(rewritten: string): Promise<string[]> {
    // TODO: replace with LLM expansion
    log('expand.rewritten', rewritten);
    const max = this.opts?.maxExpansions ?? 2;
    // naive expansions: synonyms or short variants (stub)
    const variants = [rewritten];
    if (max >= 1) variants.push(rewritten + ' detailed');
    if (max >= 2) variants.push(rewritten + ' summary');
    return variants.slice(0, max + 1);
  }

  async decompose(raw: Query): Promise<string[]> {
    // for complex queries return array of sub-queries
    // TODO: use LLM to split into semantically coherent pieces
    log('decompose', raw);
    // trivial: return raw as single sub-query
    return [raw];
  }

  async augment(raw: Query) {
    const rewritten = await this.rewrite(raw);
    const expansions = await this.expand(rewritten);
    const decomposed = await this.decompose(rewritten);
    return { rewritten, expansions, decomposed };
  }
}

// ---------------------------
// Vector Search Adapter (in-memory stub)
// ---------------------------

export class VectorSearchAdapter {
  private store: Map<string, Chunk[]> = new Map();

  // index into a collection
  index(collection: string, chunks: Chunk[]) {
    this.store.set(collection, chunks.slice());
    log('vector.index', collection, chunks.length);
  }

  // simple similarity-based stub: returns top-k by substring match + fallback
  async search(collection: string, query: string, k = 4): Promise<Chunk[]> {
    const rows = this.store.get(collection) ?? [];
    // naive scoring: containment + length
    const scored = rows.map((c) => {
      let score = 0;
      if (c.text.toLowerCase().includes(query.toLowerCase())) score += 10;
      // shorter distance roughly better
      score += Math.max(0, 5 - Math.abs(c.text.length - query.length) / 100);
      return { ...c, score } as Chunk;
    });
    scored.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
    const res = scored.slice(0, k);
    log('vector.search', collection, query, 'returned', res.length);
    return res;
  }
}

// ---------------------------
// Reranker (LLM-based stub)
// ---------------------------

export async function rerankWithLLM(chunks: Chunk[], query: string): Promise<Chunk[]> {
  // TODO: call light LLM to score relevance; here we rely on existing score
  log('rerank', 'query', query, 'candidates', chunks.length);
  // stable sort by score desc
  return chunks.sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
}

// ---------------------------
// Agent Planner (stub)
// ---------------------------

export class AgentPlanner {
  constructor(private opts?: { defaultCollection?: string }) {}

  async plan(userId: UserId, augmented: { rewritten: string; expansions: string[]; decomposed: string[] }): Promise<Plan> {
    log('planner.plan', userId, augmented.rewritten);
    // naive planner: search top collection with rewritten and expansions, then synthesize
    const steps: PlanStep[] = [];
    const collection = this.opts?.defaultCollection ?? 'default_docs';
    steps.push({ type: 'rewrite' });

    // We'll add a search step for each expansion (capped)
    for (const q of augmented.expansions.slice(0, 3)) {
      steps.push({ type: 'search', collection, query: q, k: 4 });
    }

    // final synthesis step
    steps.push({ type: 'synthesize' });

    // optionally save memory
    steps.push({ type: 'save_memory' });

    return { steps, thoughts: 'baseline planner: expand->search->synth->save' };
  }
}

// ---------------------------
// Tool Executor (stub + validator + audit)
// ---------------------------

export class ToolExecutor {
  constructor(private toolset: Record<string, (args: any) => Promise<ToolResult>>) {}

  async execute(call: ToolCall): Promise<ToolResult> {
    log('tool.execute.request', call.name, call.args);
    const fn = this.toolset[call.name];
    if (!fn) {
      const err = `tool ${call.name} not found`;
      log('tool.execute.error', err);
      return { success: false, error: err };
    }

    // naive param validation (can be extended with JSON Schema)
    try {
      const res = await fn(call.args);
      log('tool.execute.success', call.name);
      return res;
    } catch (e: any) {
      log('tool.execute.exception', e?.message ?? String(e));
      return { success: false, error: e?.message ?? String(e) };
    }
  }
}

// Example toolset (mock)
export const exampleToolset = {
  price_checker: async (args: any) => {
    // simulate a price lookup
    if (!args?.sku) return { success: false, error: 'missing sku' };
    return { success: true, output: { sku: args.sku, price: 123.45, currency: 'USD' } };
  },
  get_weather: async (args: any) => {
    if (!args?.city) return { success: false, error: 'missing city' };
    return { success: true, output: { city: args.city, high: 24, low: 16, condition: 'Sunny' } };
  }
};

// ---------------------------
// Memory Store (short + long term stub)
// ---------------------------

export class MemoryStore {
  // short-term (working buffer), keyed by user + task id
  private shortTerm = new Map<string, any[]>();
  // long-term vectorized store (we store summaries as text here)
  private longTerm = new Map<string, { id: string; summary: string; meta?: any }[]>();

  pushShort(userId: UserId, item: any) {
    const arr = this.shortTerm.get(userId) ?? [];
    arr.push(item);
    this.shortTerm.set(userId, arr.slice(-50)); // cap
    log('memory.pushShort', userId);
  }

  readShort(userId: UserId) {
    return this.shortTerm.get(userId) ?? [];
  }

  async saveLong(userId: UserId, summary: string, meta?: any) {
    const arr = this.longTerm.get(userId) ?? [];
    arr.push({ id: `${Date.now()}`, summary, meta });
    this.longTerm.set(userId, arr);
    log('memory.saveLong', userId, 'total', arr.length);
    return arr[arr.length - 1];
  }

  async queryLong(userId: UserId, query: string): Promise<{ id: string; score: number; summary: string }[]> {
    // naive text match
    const arr = this.longTerm.get(userId) ?? [];
    const scored = arr.map((r) => ({ id: r.id, summary: r.summary, score: r.summary.includes(query) ? 10 : 0 }));
    scored.sort((a, b) => b.score - a.score);
    return scored.slice(0, 5);
  }

  // pruning example
  pruneLong(userId: UserId, keep = 10) {
    const arr = this.longTerm.get(userId) ?? [];
    if (arr.length <= keep) return;
    this.longTerm.set(userId, arr.slice(-keep));
    log('memory.pruneLong', userId, 'kept', keep);
  }
}

// ---------------------------
// LLM generation stub
// ---------------------------

export async function generateFinalAnswer(opts: { promptTemplate: string; retrieved: Chunk[]; toolsOutput?: any[]; history?: any[]; userId?: UserId; rawQuery?: string }): Promise<string> {
  // TODO: call production LLM (e.g., OpenAI chat completion) here
  log('generateFinalAnswer', 'retrieved', opts.retrieved.length);
  const pieces = opts.retrieved.map((c, i) => `(${i + 1}) ${c.text.slice(0, 200)}`);
  const tools = (opts.toolsOutput ?? []).map((t, i) => `tool[${i}]=${JSON.stringify(t)}`);
  return `ANSWER DRAFT:\nQuery: ${opts.rawQuery}\n\nRetrieved:\n${pieces.join('\n')}\n\nTools:\n${tools.join('\n')}`;
}

// ---------------------------
// Orchestrator - glue everything
// ---------------------------

export class Orchestrator {
  constructor(
    private augmentSvc: QueryAugmentService,
    private vectorAdapter: VectorSearchAdapter,
    private planner: AgentPlanner,
    private toolExecutor: ToolExecutor,
    private memory: MemoryStore
  ) {}

  async handleUserRequest(userId: UserId, rawQuery: Query) {
    log('handleUserRequest.start', userId, rawQuery);

    // 1. augment
    const augmented = await this.augmentSvc.augment(rawQuery);

    // 2. plan
    const plan = await this.planner.plan(userId, augmented);
    log('plan', JSON.stringify(plan, null, 2));

    // containers for results
    const searches: Chunk[] = [];
    const toolOutputs: any[] = [];

    // 3. execute steps
    for (const step of plan.steps) {
      if (step.type === 'rewrite') {
        // already performed; no-op
        continue;
      }
      if (step.type === 'search') {
        const rawResults = await this.vectorAdapter.search(step.collection, step.query, step.k ?? 4);
        const reranked = await rerankWithLLM(rawResults, step.query);
        searches.push(...reranked);
      }
      if (step.type === 'call_tool') {
        const res = await this.toolExecutor.execute(step.tool);
        toolOutputs.push(res);
      }
      if (step.type === 'synthesize') {
        // limit chunks to top 4 unique by id
        const top = dedupeChunks(searches).slice(0, 4);
        const answer = await generateFinalAnswer({ promptTemplate: '', retrieved: top, toolsOutput: toolOutputs, history: this.memory.readShort(userId), userId, rawQuery });
        // push to short term memory
        this.memory.pushShort(userId, { query: rawQuery, answer, at: nowIso() });
        // continue loop; save_memory step will persist summary
      }
      if (step.type === 'save_memory') {
        // save a short summary into long-term memory
        const summary = `Q: ${rawQuery} | A: (summary candidate) saved at ${nowIso()}`;
        await this.memory.saveLong(userId, summary, { source: 'auto' });
      }
    }

    // final assembled output (for this skeleton we return last short-term answer)
    const last = this.memory.readShort(userId).slice(-1)[0];
    log('handleUserRequest.end', userId);
    return last ?? { query: rawQuery, answer: 'no answer generated' };
  }
}

function dedupeChunks(chunks: Chunk[]) {
  const map = new Map<string, Chunk>();
  for (const c of chunks) {
    if (!map.has(c.id)) map.set(c.id, c);
  }
  return Array.from(map.values()).sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
}

// ---------------------------
// Example wiring for local dev / tests
// ---------------------------

export function createExampleOrchestrator() {
  const augment = new QueryAugmentService({ maxExpansions: 2 });
  const vector = new VectorSearchAdapter();
  const planner = new AgentPlanner({ defaultCollection: 'default_docs' });
  const toolExec = new ToolExecutor(exampleToolset);
  const memory = new MemoryStore();

  // seed vector store with simple chunks
  vector.index('default_docs', [
    { id: 'c1', text: 'How to set up RAG with vector DB and chunking', docId: 'doc1' },
    { id: 'c2', text: 'Chunking strategies: recursive, semantic, hierarchical', docId: 'doc1' },
    { id: 'c3', text: 'Agents coordinate tools, memory and retrieval for robust systems', docId: 'doc2' },
    { id: 'c4', text: 'Tool calling uses JSON schema and parameter validation', docId: 'doc3' }
  ]);

  const orchestrator = new Orchestrator(augment, vector, planner, toolExec, memory);
  return orchestrator;
}

// ---------------------------
// If run directly (node ts-node) demo
// ---------------------------

if (require.main === module) {
  (async () => {
    const orch = createExampleOrchestrator();
    const res = await orch.handleUserRequest('user_1', 'how to build rag system for docs');
    console.log('demo result:', res);
  })();
}

工程注意事项

  • 每一步都应有超时/重试策略,避免单点阻塞。
  • 保持端到端可观测性:请求 id、每次检索/工具调用的 latency、返回的 top-k 文档 id、LLM token cost、agent 决策日志。
  • 在生产中把 LLM 调用结果(尤其是 rerank 分数、工具观测)写进追踪系统,方便后期调优与审计。

四、落地实践清单(Checklist & 经验指南)

  1. 分块策略先试验再决定:先用 Recursive 或 Document-based 作为 baseline;对长报告或法规文本考虑 hierarchical / late-chunking。
  2. 查询增强放在检索前:实现 rewrite + 1–3 个 expansion;对复杂 query 做 decomposition。
  3. 检索后做 rerank:用小模型或 LLM 对 top-N 做二次排序,保留 2–4 个最相关 chunk 注入 prompt。
  4. Prompt 模板工程化:把 system prompt、tool 描述、few-shot 示例作为可配置文件维护;版本化管理。
  5. Memory 策略化:实现 scoreMemory() 和 pruneMemory() 两个定时任务;长期保存摘要而非冗长日志。
  6. Tool 安全层:工具调用前校验参数、权限、并记录调用链。
  7. Agent 可解释性:把 agent 的“thoughts/plan/decisions”记录出来,方便排错与合规稽查。
  8. 性能与成本衡量:监控 LLM token 用量、向量搜索次数、工具调用次数并以 SLO/成本目标调整返回粒度与策略(例如缓存、高频意图答复用固定模板)。
  9. 用户体验:在不确定或信息不足时,优先用“澄清问题”的策略而非盲答;在展示结果时给出来源与置信度(“基于 X 文档”)。

五、常见场景实战示例(简短)

场景 A:企业文档问答(Legal / Policy)

  • Chunking:文档-based + hierarchical(章节摘要 + 段落 chunk)

  • Query flow:rewrite → expand → search → rerank → CoT + cite sources

  • Memory:只保存用户偏好与重要 follow-up 任务

场景 B:客服机器人(多轮会话)

  • Memory:强化 episodic(过往对话摘要)

  • Agent:判断是否需要工具(创建 ticket、查询订单)

  • Tools:严格权限与幂等性(创建 ticket 要返回 id,支持回滚)

场景 C:数据分析助理(需要 DSL 或 SQL)

  • Decompose:把用户问题拆成子查询(列名/过滤器/汇总)
  • Tools:调用数据库/可视化 API(工具参数必须严格范式化)
  • ReAct:在需要时交替“思考(SQL)— 执行(运行 SQL)— 观察(结果)— 决定下一步”

六、常见坑与对策(工程维护角度)

  • 坑:上下文污染(hallucinated facts in memory)

    对策:在写入长期记忆前用 LLM 做“反思”评分,并要求高置信度+来源,或只保存摘要和来源引用。

  • 坑:检索到的 chunk 太长或太短

    对策:把 chunk 大小/重叠设为可配置参数,A/B 测试不同策略(比如 300–800 tokens),并监控检索命中率与生成质量。

  • 坑:工具误用或越权

    对策:工具描述 + 执行层参数校验 + 权限层 + 审计日志。

  • 坑:成本失控(频繁 LLM 调用)

    对策:缓存常见问答、对低价值请求使用小模型或模板、批量化检索/合成。


七、结论与下一步路线(给工程团队的行动建议)

  1. 先搭一个最小可运行的 pipeline(MVP) :Pre-chunk + vector DB + basic query-rewrite + rerank + LLM generation + simple memory(保存摘要)。验证端到端效果并测稳定性与成本。
  2. 把 agent 放在 orchestration 层:开始时做简单 rule-based agent,再逐步替换为 LLM-based planner(带解释日志)。
  3. 引入观测与回路:收集检索质量、rerank 分数、用户反馈,搭建闭环优化策略(自动调整 chunk 大小、rewrite 模式)。
  4. 定义 memory policy:明确哪些信息入库、如何打分、何时 prune。
  5. 工具接口标准化:结构化描述工具并在 system prompt 内声明,执行层做校验与审计。

参考(基于电子书核心观点的工程化实现)

  • 把“上下文卫生”作为长期工程目标:Quality validation、Summarization、Pruning、Offloading。
  • 采用动态 orchestration(Agent)而非静态 pipeline。
  • 深入优化分块(chunking)策略,并用后分块或 hierarchical chunk 在需要时恢复上下文。
  • 通过 Query Augmentation(rewrite/expand/decompose)显著提升检索召回与精度。
  • 记忆系统要有“写入门槛”和“维护任务”,以避免污染。
  • 工具使用以函数调用/JSON schema 为主,并且要可审计、可回滚。
  • 原文-- 8738733.fs1.hubspotusercontent-na1.net/hubfs/87387…