找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2618

积分

0

好友

351

主题
发表于 昨天 17:10 | 查看: 0| 回复: 0

科技感十足的人脑与数据可视化图,代表人工智能与数据分析

最近,我花了一周左右的时间,为一个内部的传统研发平台接入了 AI Agent 开发能力。很多同学都对 Agent 的底层实现非常好奇,所以这篇文章就来分享一下我的具体做法,希望能给想要自建 Agent 的开发者一些启发。

几点说明:因人力有限,有些方案细节未做深度评测,而是直接选择了业界实践较多的成熟方案。本文主要侧重于核心思路和上下文管理的过程。文中使用了一些内部平台的基础能力(如 RAG、代码管理、DeepWiki等),外部开发者如需实现,需要自行寻找相应的替代品。

背景简介

“奥德赛研发平台”是 ICBU 买家技术团队使用的 TQL(淘宝基于开源 GraphQL 的定制版本)研发平台。大量开发者会在这个平台上编写 TQL 脚本来实现 BFF(Backend For Frontend)接口。

近一年来,各种 AI Coding 工具层出不穷。在深度体验了 Cursor、Claude Code 等顶尖产品后,我深感它们极大地解放了我在前端开发上的生产力。于是,我就想:能不能让团队的后端同学们也“吃好点”,告别纯手写代码的模式呢?就这样,针对 BFF 场景的 AI 助手——“小D同学”应运而生。

技术选型

原来的平台界面长这样,开发者可以在上面完成编码、调试、发布等一系列工作:
奥德赛研发平台GraphQL工作台界面截图

要在这样一个现有的平台内集成一个 AI Agent,并且让它能够感知前台的页面环境,甚至对页面进行操作,通常有三种实现方式(假设都采用 AI 辅助开发):

如果只是想做一个普通的对话型 Agent,直接用一些开放的应用平台搭建即可,不必自己从头开发。

三种Agent集成方案对比表格

综合评估了三种方案的利弊后,我们决定优先保证用户体验和开发效率,最终选择了第三种方案。

宿主页面 Iframe 嵌入 Agent

宿主页面与Iframe Agent集成示意图

整体的数据流转思路如下:

  1. 宿主页面向 Agent 暴露上下文信息(如脚本内容、请求参数、调试结果)以及操作页面的工具接口(如脚本执行、脚本Diff预览)。
  2. Agent 感知宿主页面的上下文环境,然后请求后端服务,并将内容编辑、脚本执行等工具调用的 action 推送给宿主页面。

流程概览图如下:
Agent与宿主页面交互流程图

应用框架选择

在应用框架的选择上,我不做过多对比,直接给出我的选择。它们不一定是最好的,但非常契合我当前的场景。

集团 FaaS 基建
简介:FaaS(Function as a Service,函数即服务)平台是一个面向研发人员的全托管、事件驱动、弹性伸缩的 Serverless 计算基础设施。其核心目标是让开发者只关注业务逻辑代码本身,无需操心服务器运维、资源扩缩容、中间件对接等底层细节。
划重点:FaaS 让你只关注代码,免运维。这正是当前这种轻量级 Agent 服务所需要的。

Next.js + React
为什么选择 Next.js + React 作为前后端应用框架?

  1. 集团内部有现成的前后端一体化框架可以直接使用。
  2. 前后端共用一套开发语言(Node.js/JavaScript),并集成在一个应用中,这对 AI Coding 非常友好——谁用谁知道。可以真正做到只关注功能需求,让 AI 帮你完成具体实现。

LangGraph
LangChain 团队在 Workflow/Agent 领域深耕数年,高度抽象了 Agent 的开发模式。LangGraph 的巧妙设计让你可以轻松构建一个状态图。你只需关注系统提示词、工具节点(通常是 MCP 工具),就能实现一个具备自主决策能力的 Agent,对“手残党”非常友好。
基础的Agent执行循环示意图

方案落地

应用框架初始化

如上文所述,结合当前场景,将 LangGraph 抽象的状态图具体化,替换成我们自己的工具,就得到了下面的图和伪代码:

此处了解个大概即可,稍后会详解工具部分。

BFF Agent 状态图:
BFF Agent状态流程图

伪代码实现:

import { StateGraph, END, START } from '@langchain/langgraph';

// 创建状态图
const agentGraph = new StateGraph({
  channels: {
    messages: {
      reducer: (prev, next) => [...prev, ...next],
      default: () => [],
    },
  },
});

// 添加节点
agentGraph.addNode('agent', async (state) => {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
});

agentGraph.addNode('tools', async (state) => {
  const lastMsg = state.messages[state.messages.length - 1] as AIMessage;
  const toolMessages = [];
  for (const toolCall of lastMsg.tool_calls || []) {
    const result = await tools
      .find((t) => t.name === toolCall.name)
      .invoke(toolCall.args);
    toolMessages.push(
      new ToolMessage({ content: result, tool_call_id: toolCall.id }),
    );
  }
  return { messages: toolMessages };
});

// 设置边
agentGraph.addEdge(START, 'agent');
agentGraph.addEdge('tools', 'agent');

// 条件边:根据是否有 tool_calls 决定路由
agentGraph.addConditionalEdges(
  'agent',
  (state) => {
    const lastMsg = state.messages[state.messages.length - 1] as AIMessage;
    return lastMsg.tool_calls?.length > 0 ? 'tools' : END;
  },
  { tools: 'tools', [END]: END },
);

// 编译图
const graph = agentGraph.compile();

参考 LangGraph 的官方文档,使用 Cursor 这类 AI 编程工具可以轻松实现上述基础的有向、有环状态图。至此,BFF Agent 服务的骨架就搭建好了。在模型选择上,由于需要生成代码,我直接使用了 Claude 模型(Claude-4.5-haiku)。

那么,接下来的重点就是从“能用”到“好用”,后续的内容非常关键,也就是“上下文工程”的优化。 接下来的优化点不分先后顺序,因为上下文工程是一个综合命题。例如,你往往在优化完系统提示词后,发现某些工具调用不符合预期,又得回过头来调整提示词。不同类型的上下文常常交叉影响,需要根据具体场景仔细甄别。
Context Engineering维恩图

系统提示词优化

Anthropic 官方推荐了一系列提示词优化技巧,非常有效!下面介绍我高频用到的几个基础技巧,更多技巧强烈建议直接学习原文。

  1. 角色设定:角色设定可以显著提升模型在特定任务上的性能(参考 Qwen 的 MOE 机制),并改善模型的注意力,帮助它发现更多关键问题。
  2. 使用 XML:Anthropic 官方推荐使用 XML 标签构建提示词。实测非常有效。当你遇到模型不遵循指令,或上下文过长导致模型出现遗忘/幻觉时,可以尝试将提示词格式更换为 XML。
  3. 使用示例(Few-Shot):给出少量准确的示例,对于改善输出内容的质量有重大帮助。此外,一篇研究指出,尽量直接给模型正例而不是反例,以保护模型的注意力。(例如,告诉你“不要去想一会吃什么”,你反而会刻意思考如何“不去想”,这就浪费了注意力)
    关于模型有意控制内部状态的说明

上述三个技巧简单且行之有效,全程指导了我的提示词优化工作。下面来看具体怎么用。

角色设定 & 使用 XML

淘宝的 TQL 本质上是一种 GraphQL 的方言,ICBU 还在 TQL 基础上做了定制,大模型原本是不会写的:

模型不了解私域知识这个问题,在自建 Agent 时往往是一个共性问题,所以才需要大量的提示词、工具和知识库。即使是强大的 Claude 或公司自研的 Qwen,最初也不知道 TQL 是什么。

模型不清楚TQL含义的对话截图1
模型不清楚TQL含义的对话截图2

好在模型不用从零开始,它会写 GraphQL。我们只需要阐述清楚二者的区别。不只是提示词,后续的知识库也在尽力向模型解释这些私域知识。“小D同学”的提示词摘要如下:

<role>你是小D同学,一个专业的TQL脚本编写助手,TQL是淘宝基于GraphQL扩展的查询语言,你只会写TQL语法,不会任何其它脚本。你的任务是帮助用户基于企业知识和用户输入,编写高质量的TQL脚本。</role>
<instructions>
  <instruction>你非常欠缺TQL知识,但好在系统内置了很多工具,你可以充分使用这些工具来辅助你完成任务。</instruction>
  <instruction>系统在开源GraphQL的基础上,扩展了很多自定义的指令,如果你遇到不确定或无法实现的需求,可充分用工具查询相关知识</instruction>
  <instruction>回答要专业、友好、有条理。使用Markdown格式输出。</instruction>
  <instruction>在编写TQL脚本时,要确保语法正确、查询结构清晰、字段选择合理。</instruction>
</instructions>
  • 角色设定上,让模型更多关注 GraphQL 方向的知识,并申明 TQL 和 GraphQL 有区别,让模型谨慎操作。
  • 指令(instructions)上,第一条指令比较有意思。在没有这条指令之前,模型会表现得过于自信(即使在 temperature=0 的最严谨状态下),拿到用户需求后直接上手写 GraphQL,出现了与 TQL 规范不符的代码。使用第一条指令弱化了模型的“信心”后,模型才开始谨慎地调用知识库来了解更多上下文信息。

对于 TQL 在 GraphQL 基础上的扩展内容,为了防止提示词内容爆炸,我做了一轮精简,只保留基础介绍部分(作为索引知识)放在提示词中,并引导模型在需要使用具体能力时,动态通过工具查询具体知识,从而保护了模型的注意力。

自定义指令部分示例:

<directives>
    非常重要!系统有大量的扩展指令,具体用法需要通过工具查询相关知识,
    在使用这些指令时,一定要先学习指令的用法,不要盲目使用。

    指令一般紧挨着字段或函数, fieldA @指令名 或 funcA(xx) @指令名 这样使用。

    以下是常用指令(格式: "指令名":"描述|参数"):
                      <![CDATA[{
      "转换类":{"unwrap":"解包/对象拍平/对象解构","toBool":"转布尔|not","encode":"编码|protocol=http","wrap":"包装","toInt":"转整数|offset","toUpperCase":"转大写","decode":"解码|protocol=http","autoflat":"自动扁平化"},
      "字符串操作":{"suffix":"添加后缀|value","prefix":"添加前缀|value"},
      "条件过滤":{"hide":"隐藏字段","filterBy":"表达式过滤|spel,aviator","include":"条件包含|if!","skip":"条件跳过|if!","default":"默认值|value"},
      "列表操作":{"index":"取元素|offset","ascBy":"升序|path"},
      "逻辑脚本":{"mapping":"映射|func","const":"常量|value,beforeExecute=false"},
      "高级扩展":{"medusa":"Medusa服务|url,language","diamond":"Diamond服务|url"}
    }]]>
  </directives>

全局函数部分示例:

<TQLFunctions name="系统内置的全局函数">
    <description>以下是TQL脚本中可直接使用的内置函数,详细用法请通过知识库查询。</description>
    <![CDATA[
    【字符串处理】
    - QL_concat: 拼接三个字符串(a, b, c参数)
    - QL_string_replace: 字符串替换(replaceText, searchString, replaceString)
    - QL_stringToList: 字符串按分隔符转列表(data, delimiter)
    - QL_stringToJSON: 字符串转JSON对象
    - QL_jsonStringify: JSON对象转字符串
    - QL_joinStringByPath: 通过JSON Path提取属性并拼接(object, path, delimiter)
    - QL_urlDecode: URL解码
    - QL_addHttpsSchema: 自动添加或转换为https协议头
    - QL_addUrlParam: 给URL添加参数(url, param)

    【数值计算】
    - QL_sumLong: 两数相加(addition1, addition2)
    - QL_subtraction: 两数相减(minuend, subtrahend)
    - QL_divideInt: 整数除法向下取整(dividend, divisor)
    - QL_random_int: 生成指定范围随机整数(min, max)

    【条件判断】
    - QL_if: 三元条件判断(condition, output, orElse)
    - QL_conditional: 复杂条件表达式,支持#env.get()获取变量(exp, params)
    - QL_defaultIfBlank: 空值时返回默认值(str, defaultValue)
    - QL_timestampComparator: 判断当前时间是否在指定时间戳范围内

    【AB测试】
    - QL_abTest: AB实验分流,返回命中的实验桶标识(experiment)
    - QL_batchAbTest: 批量执行多个AB实验

    【数据处理】
    - IDs_fromString: 从字符串解析ID对象,支持商品/类目/供应商/公司/国家(ids)【重要】
    - QL_mergeList: 合并两个列表(aim主列表, tail尾部项)
    - QL_subList: 截取列表子集(base, from, to)

    【国际化】
    - QL_medusa: 美杜莎翻译,获取国际化文案(key)【所有文案必须使用】
    - QL_countryFlag: 获取国家国旗链接(country)

    【数据脱敏】
    - QL_desensitization: 数字脱敏,末位补0(value)

    【输出与重命名】
    - TQL_output: 输出固定对象,包括布尔值、数字、数组、对象(object)
    - 字段重命名: 使用GraphQL别名 或 @hide指令隐藏原字段
    ]]>
  </TQLFunctions>

使用示例

关于示例部分,虽然上面提到要给模型正例,但我非常不建议一开始就盲目给模型一堆示例。应该先让模型自由发挥,在后续调试过程中,针对模型容易出错的部分,再直接给出正确的引导。

比如,我一开始遇到模型总是将请求参数硬编码在脚本中,没有抽离成查询变量,于是我给出了这样的示例规则:

<rule>
  <title>参数分离原则</title>
  <content>
    建议将GraphQL请求的参数单独放在variables中,但要以实际需求为主。如果用户明确要求将参数写死在脚本中,或者参数是固定的常量值,可以直接写在脚本中。
    当需要参数分离时:
    - 脚本中使用变量定义($variableName)
    - 参数值通过editVariables工具设置到variables中
    - 使用editScript和editVariables两个工具分别更新脚本和变量
    示例(参数分离):
    脚本:query($userId: String!) { user(id: $userId) { name } }
    参数:variables:{"userId": "12345"}
    这样做的好处:脚本可复用,参数和逻辑分离,便于维护和调试。
  </content>
</rule>

类似的例子还有很多,不再一一展开。还是那句话,建议仔细阅读 Anthropic 官方的优化教程。有些技巧初看可能不以为然,但当你真正遇到问题时,就能快速联想到,帮你少走弯路。

知识库建设

紧接上文,系统提示词给出了部分私域知识片段后,TQL 的详细用法(全局指令、函数)、服务端内部可用的查询接口字段、线上运行中的成熟脚本等海量知识,不可能一次性全交给模型。目前,RAG 是最成熟的知识管理方式之一。“小D同学”的知识库主要分为以下三大类:

线上热门脚本

通过对线上脚本调用量的监控采集,我们提取出 Top 100 的热门脚本。然后分别使用 Qwen 的大小模型,对脚本进行初步理解(撰写 Wiki),生成一份格式化的文档,帮助模型快速理解脚本含义。简单示例如下:
GraphQL脚本功能总结与代码示例

RAG 的分片策略在很大程度上直接影响了召回的质量。 因此,我预先对脚本进行了切分,每个脚本作为一个独立的文档,然后直接导入到内部的知识库平台(kbase)中使用。

(kbase 是内部工程团队自建的知识库平台,支持嵌入和通过 MCP 协议召回知识。)

系统内置字段

包括 TQL & ICBU 在 GraphQL 上扩展的指令、全局函数,以及服务端的领域模型字段。GraphQL 是一门支持自省的语言(服务端用注解标注了字段,这些信息可以被扫描出来)。然而经过多年发展,自省内容已长达 600 万+字符。为了让 RAG 的召回效果好,我们对数据进行了大量清洗:

  • 扫描出全局指令、全局函数、领域模型的全集,然后与线上脚本进行匹配,只保留高频(出现3次以上)使用的部分;对于语义相似的内容,人工介入区分;功能相同但语义不同的部分,选择性保留。
  • 剔除标注为废弃的内容。
  • 和热门脚本一样,预先对文档进行拆分。全局指令、函数作为独立文档,内置领域模型适当合并。

清洗过程非常耗时,需要细心和耐心,可以借助 AI 编程助手的技能,帮你编写脚本处理数据。
系统内置字段也可以用文档管理起来,方便导入到 RAG 中。

服务端代码理解

上述梳理出来的知识更多是“结果”。为了帮助模型从源头理解字段背后的含义,TQL 对应的服务端源码也是很好的输入。这部分已经有成熟能力可以直接使用,如内部代码平台的搜索工具,以及内部的 DeepWiki 平台。由于此前已经在 DeepWiki 上解析过服务端应用(winterfell)的源码,且实测下来其代码库(codebase)搜索效果比较理想,所以选择了它提供的能力来做代码片段召回。

工具接入

回过头再来看看 Agent 的工具设计,分为两部分:本地工具和远程(MCP)工具。

远程(MCP)工具

  • kbase 的 MCP Server
    知识库MCP工具searchDocChunk界面
  • DeepWiki 的 MCP Server
    DeepWiki MCP工具界面

远程工具主要用来召回上文“知识库建设”部分的内容。由于它们提供的工具比较全,而我实际只会用到其中一部分,所以在系统中设计了白名单机制,只加载白名单内的工具。还是那句话:节约上下文,保护模型注意力。
MCP服务器配置代码示例

提示:MCP 的鉴权认证需要自行参考官方文档,此处不赘述。

本地工具

  • 编辑脚本(editScript)
    • 功能:编辑/更新 GraphQL 脚本内容
    • 输入script (string) - 完整的 GraphQL 脚本代码
    • 输出:返回更新状态
  • 编辑变量(editVariables)
    • 功能:编辑/更新 GraphQL 请求变量(mock 参数)
    • 输入variables (string) - 完整的 variables JSON 字符串
    • 输出:返回更新状态
  • 执行脚本(executeScript)
    • 空方法,用于引导模型择机执行 GraphQL 脚本,实际执行逻辑在前端宿主页面上。
  • 验证结果(validateResult)
    • 功能:验证 GraphQL 脚本的执行结果
    • 输入
      • prompt (string) - 验证要求描述
      • currentScript (string, optional) - 当前脚本
      • currentVariables (string, optional) - 当前变量
      • 执行结果通过闭包注入(不在参数中传递)
    • 输出:返回结构化验证结果(passed、summary、problems、suggestions、needMoreContext、contextQueries 等)

工具调用链路

将这些工具注册到 Agent 中后,常规的工具调用链路如下:
用户请求处理与工具调用决策流程图

服务端内部的工具调用比较好理解,但服务端的工具调用如何触发前端 UI 侧的工具调用呢?

例如,当 Agent 调用【执行脚本】方法时,本质上还需要前端页面响应——点击执行按钮,然后将执行结果回传给 Agent,Agent 才能继续处理。

聪明的你可能已经想到了:前后端使用全双工通信,维护一个长连接。服务端调用 executeScript 时,实际上什么也不做,就等待前台页面执行完并将结果传回服务端,服务端再继续后续的状态流转。

是的,很多带 GUI 的工具确实是这么处理的(在 LangGraph 中称之为“中断”,也就是常说的 HITL - Human in the Loop)。

遗憾的是,FaaS 在设计之初并不支持长连接:
FaaS无状态、按需执行、短生命周期特性说明
在配置界面上,函数能设置的最长保活时间是 300 秒(默认为 50 秒)。
FaaS函数配置界面截图
所以,我们肯定得“曲线救国”了。请允许我先卖个关子 😁。

上下文管理

其实,具备了上述能力后,我们已经有了一个可以帮助用户编写脚本、执行、验证的智能体了。但是,还面临两个非常现实的问题:

  1. 模型本身不支持连续对话,会话需要自己做持久化管理。
  2. 尽管前期做了大量节约上下文窗口的优化工作,但真正处理复杂脚本时,模型的上下文窗口还是会快速膨胀,导致注意力变得稀疏,出现幻觉、准确率下降、用户体验变差。

一轮简单的对话,只要包含工具调用,就可能消耗掉 1/4(约 5 万)的 Token。因为在默认设计中,每个节点返回的消息都会被拼接到发送给 LLM 的消息列表中。
Agent多轮对话与工具调用界面截图

连续对话

先来处理简单的。连续对话的实现非常简单,也可以参考 LangGraph 的 checkpoint 设计,支持从任意节点重新开始。BFF Agent 暂时没用到这个高级能力。

具体实现是:在用户开始一次新对话时,创建一个 sessionId,用来关联存储消息列表。每次消息列表发生变化时,都持久化存储起来(这里我用了 Tair)。当用户有新输入时,直接用 sessionId 取出历史消息列表进行拼接,合并后发给大模型,即可实现连续对话。

UI 工具是如何调用的?

在支持连续对话的基础上,诞生了一个非常巧妙的设计:

  1. Agent 的接口设置为 SSE(Server-Sent Events)方式。服务端收到请求后,流式向前端推送分片消息,同时将每轮消息持久化存储。(SSE 只支持服务端单向向客户端推送消息。)
  2. 当调用到需要前台 UI 响应的工具(如 executeScript)时,在把工具调用信息输出给前端后,直接退出 LangGraph 的状态流转,结束此次请求。是的,直接结束。
  3. 前端特殊处理该类工具调用后(例如,展示脚本 Diff 并让用户接受或拒绝 Agent 的修改、执行脚本等),增加一条隐藏消息(如:“【脚本执行成功,结果是xxx,请继续处理】”)重新调用 Agent 的流式接口。接口内部取出之前持久化的消息内容,拼接上这条隐藏消息,从头开始初始化 AgentGraph 进行调用。

用户在前台看到的是连续的交互:
用户视角的商品信息分析与脚本执行界面

实际上发送给模型的消息流包含了隐藏的系统消息:
实际包含iframeContext和隐藏消息的对话数据

至此,我们就实现了一个可以自主执行、验证、修复、再执行的智能体。

相应地,会话中断恢复也不难处理。因为服务端完整缓存了会话内容,中断后,用户只需发送新消息,接口内部会自动拼接上之前的会话历史,重新进入 Agent 的状态流转。

会话压缩

解决了连续对话的问题,上下文窗口超限的问题怎么办呢?目前的答案是压缩,只保留摘要信息。(不知道未来模型是否可以自行处理超长上下文~)

压缩工具响应结果

既然调用外部工具如此耗费 Token,那么我们可以先将工具的调用结果缓存起来,再用一个专门的工具函数去精准检索内容,并且只把检索后的、精简过的内容放到消息列表中,这样就能极大缓解问题了。

于是,我在 Agent 内部增加了“工具结果缓存 + 检索详情”的工具(summaryToolResult)。在工具调用结束后,增加了如下处理机制:
工具执行结果缓存与摘要处理流程图

当然,summaryToolResult 的任务非常明确:根据问题从大段原文中提取相关信息。普通的模型也有不错的据实回答能力,所以这里选择更轻量的 Qwen 模型来做检索召回,还能节省不少成本。

为了让 summaryToolResult 的回答尽可能结构化且保留原始关键信息,我对它的提示词进行了优化,强制要求使用 XML 格式响应。 没错,用的还是 Anthropic 优化提示词的那一招。

(此处省略了非常长的 summaryToolResult 提示词示例,它详细规定了提取不同类型知识(如全局函数、领域模型字段、内置指令)时必须输出的 XML 结构。)

这样,即使是 Qwen 模型,也能高质量地输出结构化的信息。工具输出示例如下,包含了具体字段的名称、描述、类型、参数等:

<field>
  <name>freight</name>
  <type>复合类型</type>
  <description>物流模型</description>
  <arguments>
    <arg required=\"false\">
      <name>dispatchCountryCode</name>
      <type>String</type>
      <description>发货地代码</description>
    </arg>
    <arg required=\"false\">
      <name>needAlibabaGuaranteed</name>
      <type>Boolean</type>
      <description>是否返回半托管物流信息,false不返回,null 或 true均返回</description>
    </arg>
    <arg required=\"false\">
      <name>moqType</name>
      <type>String</type>
      <description>MOQ档位,实验推全,全部第一档 first</description>
    </arg>
  </arguments>
</field>

对工具响应进行优化后,同样一个问题,主 Agent 的上下文窗口占用只需要花费不到原来 1/10 的 Token 就能解决了(从约 5 万下降至约 4 千)。

上下文压缩

但即便对工具响应进行了压缩,仍然可能遇到上下文窗口超限的问题。为什么 Cursor、Claude Code 等产品好像从来没给用户暴露过这类问题呢?

GitHub 上有一个 Claude Code 的逆向工程,秘密就藏在上下文自动压缩的机制里(Claude-Code-Reverse)。

比较靠谱的说法是,有两个触发自动压缩的时机:

  1. 上下文窗口使用即将超限。(据传 Claude Code 是在窗口使用超过 82% 时触发自动压缩)
  2. 新对话与历史对话毫无关联。

因为当前场景下 Agent 的负担还算比较轻,所以我只选择了第一种压缩机制,就足够用了。
Agent上下文压缩触发与处理流程图

因为上下文压缩实际上是有损的,一旦触发压缩,Agent 就似乎“忘记”了之前的任务细节,所以 Claude Code 在压缩时非常谨慎:
Claude Code压缩提示词的结构化说明

因此,BFF Agent 也直接复用了这份提示词进行压缩。在此基础上,为了使用户近期的对话被完整保留,避免一旦压缩就瞬间遗忘的现象,我在压缩时默认会完整保留自用户近 3 轮对话开始之后的所有消息。

最终的线上配置为:

{
  enabled: true, // 启用压缩
  dangerThreshold: 80, // 1-100 当上下文窗口使用超过80%时触发压缩
  keepRecentRounds: 3, // 保留最近3轮用户对话开始之后的消息不被压缩
}

消息压缩的图示如下:
消息压缩时序示意图

到这里,基础的优化工作就基本结束了。后续更多的优化,就需要采集用户的 Bad Case,再不断迭代优化提示词、工具和知识库。文章有点长,对于熟练的同学来说可能包含许多已知内容,但还是希望能给正在探索自建 Agent 的开发者带来一些切实的帮助。

欢迎在云栈社区交流讨论更多关于AI应用开发、后端架构以及大模型工程化实践的话题。

材料与参考链接




上一篇:Spring Boot、Quarkus、Vert.x...云原生时代Java Web框架怎么选?锐评13个框架
下一篇:Linux系统/etc目录详解:运维必知的核心配置文件与作用
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-31 00:23 , Processed in 1.330728 second(s), 46 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表