我的工作流

0 阅读7分钟
graph TB
    A["Agent"] --> U["Utility.app"]

    U --> M["本地 MCP 服务"]
    U --> P["Proxy 网关"]
    U --> G["依赖图"]
    U --> K["知识面板"]

    M --> D1["记忆体"]
    M --> D6["内置 Tools"]
    
    P --> D8["动态提示词注入"]
    P --> D9["Token 分析治理"]

    K --> D4["知识池"]

为了保证留存率,我会先问你几个问题,在使用 Agent 的过程中:

  • 是否遇到过跨会话上下文丢失、前后理解不一致?
  • 希望 Agent 记住对话中产生的记忆、内容?
  • 是否遇到过长上下文污染?越长的会话,模型遵从率、智能程度越低?
  • Agent 在开发过程中,是否需要经常检索项目,对项目缺少全局视野?
  • Agent 与上游 LLM 之间是否是黑盒系统,较难感知内部?Token 消耗未知,无法针对性治理?
  • Agent 使用多了,是否觉得探索知识的欲望、主动阅读文章、源码的意愿更低了?

如果有遇到过这些问题,可以看看我是怎么搭建自己的工作流的。

另外说下,我使用的工具是:Claude Code + Utility.app 接入 LLM API。

Utility.app 是我自用的一个核心工具。

记忆体,跨上下文理解的基石

当使用多个 Agent 时、当跨会话开发时、当会话内产生知识时,你会想到需要什么?

我的回答是:共享记忆体。我们需要一个能够记录工作进度、对话知识的记忆体,所有消费层使用同一个记忆体,就像人脑一样,不管做什么总是从一个记忆体出发,形成行为的一致性。

那么对于 Agent 来说,记忆体的最佳形态是什么?

本地 MCP 服务,MCP 协议被设计为可拔插的,可以随意接入任何支持了 MCP 协议的 Agent,供它们消费。

拥有了这两样东西,我们可以解决两件事:

  1. 上下文保留、理解一致
  2. 知识的存储

这还只是明面上的价值,在背后,我们可以思考、分析这些记忆,我们可以提炼记忆进行沉淀,产出更优质的内容;也可以从记忆中生成报告等等。

落到实际,则表现为三层存储记忆架构:

graph TB
    A["Agent"] -->|"tools/call"| M["MCP 服务"]
    M -->|"结果"| A

    M --> R["recall"]
    M --> W["remember"]

    subgraph MEM["三层记忆"]
        H[&#34;热层<br/>会话记忆&#34;]
        C[&#34;冷层<br/>长期知识&#34;]
        N[&#34;Markdown 笔记&#34;]
    end

    W --> H
    R --> H
    R --> C
    R --> N

热层保留温热的、新的记忆;冷层保留长期有用的记忆;最终沉淀到笔记层,目的是永久存储与方便人工阅读。

remember / recall 是本地 MCP 服务内置的两个工具,用于记忆的写入与检索,Agent 通过调用这两个工具来操作记忆。

Proxy 网关,扩展的关键

记忆体解决了跨会话理解、多 Agent 共享记忆的问题,我们现在聚焦到长上下文污染与黑盒系统的问题。

黑盒系统破局需要 Proxy 网关很好理解,在 Agent 与上游 LLM 厂商插入一层代理层,就可以使黑盒系统透明,那么长上下文污染为什么会需要代理层呢?

我们知道,在长对话中,初始的系统提示词、规则、或者是工具描述,非常容易被后续的上下文污染,导致模型的注意力更关注最新内容,而忽略了系统提示词、规则、工具描述,造成的后果可能是:

  • 行为漂移
  • 遵从率下降
  • 工具调用意愿下降
  • 智能降低

这种时候一般做法是新开会话,我认为这很麻烦,所以我找到了另一个方式:动态提示词注入。

graph TB
    A[&#34;Agent&#34;] -->|&#34;请求&#34;| P[&#34;Proxy 网关&#34;]
    S[&#34;提示词&#34;] -->|&#34;读取规则&#34;| P
    P -->|&#34;注入提示词&#34;| B[&#34;每条真实 user 消息&#34;]
    B -->|&#34;转发&#34;| U[&#34;上游 LLM&#34;]
    U -->|&#34;响应&#34;| P
    P -->|&#34;响应&#34;| A

它的工作原理很简单,既然 LLM 对于旧的消息注意力下降,那就将核心的提示词注入到每条最新的用户消息中,使其具有最高优先级。

为什么是注入到用户消息中呢?而不是到系统提示词位置?这个决策来自长久的观察,在使用 DeepSeek 这种较智能的 LLM 时,注入不缓存的系统提示词通常不会有太大的问题,但对于像 小米 mimo 这种智能程度较低的 LLM 来说,它更愿意遵守最新的用户消息。

这种方式的效果非常好,但代价也很明显,由于每轮用户消息都注入同样的内容,会增加 Token 消耗,所以一般动态注入的提示词偏向轻量、核心、全局可用,对于项目生效的提示词而言,更适合用 AGENT.md/CLAUDE.md。

Token 分析治理,省钱必做的一步

代理还有一个核心的价值是黑盒系统可观测了,我们可以记录每个请求的输入输出、模型的请求成功率、延迟、缓存率,这些指标具有很高的价值,我们可以根据这些内容来分析、量化 Token,并据此进行针对性的治理。

在漫长的治理过程中,我发现核心的就几点:

  • 提高缓存率
  • 不破坏缓存
  • 减少输入

不破坏缓存是一个很重要的前提,代理层能够在请求转发到上游前操作消息,一般能想到的做法是在转发前压缩、裁剪、剔除消息,以期达到减少 Token 输入的目的,这种做法是不完全正确的,因为这会破坏缓存,使每条断了缓存的消息都以非缓存的价格计费。

在进行 Token 治理时,应该从成本的维度去分析,而不是 Token 输入的多少。

也就是说,当大量缓存输入的成本超过因裁剪破坏缓存的成本时,是可以考虑裁剪历史消息的。

减少输入就很好理解了,Agent 在使用过程中通常会进行多次工具调用,这些工具会产生大量的Token 输入,有些可能只使用一次,却在后续的每次对话中都携带,即使以缓存价格计算,但数量暴增时也会导致成本的增加。

所以我们需要针对工具输出内容进行瘦身,比如 Bash 只看是否有 warning/error。

我做的 Token 治理有:

  • 减少重复内容:同一会话反复获取到同一份工具输出内容时,第二份起替换成一句短提示,保留第一次的全文。
  • 裁剪 shell 输出:超长命令输出保留前 70% 和后 30%,中间插入截断标记;JSON 做无损压缩。
  • 注入保证幂等性:前置注入保证幂等性,不破坏缓存
  • 工具限流:减少内置工具的默认响应
graph TB
    A[&#34;Agent&#34;]

    A -->|&#34;模型请求&#34;| P[&#34;Proxy 网关&#34;]
    P --> Q[&#34;请求侧治理<br/>规则注入 · 内容去重 · Shell 瘦身&#34;]
    Q --> U[&#34;上游 Provider&#34;]
    U --> R[&#34;响应侧治理<br/>Usage 记录 · 响应注记 · 上下文提示&#34;]
    R --> D[&#34;用量库&#34;]
    R --> A

    A -->|&#34;工具调用&#34;| M[&#34;MCP 服务&#34;]
    M --> L[&#34;工具返回限流&#34;]
    L --> A

依赖图,全局感知视野

使用过 Claude Code 这类 Agent 开发项目的应该都有一个感受,缺乏全局视野的感知能力,可能这次实现了一个组件、工具,下次有相同需求时就忘了,并重复实现;或者经常需要全局查找某个组件、工具。

这是因为缺失全局视野能力的原因,或者说是整个项目的依赖图。

针对这个痛点,我给 Utility.app 加了一个扫描依赖图的功能,表现为 cli 命令:

utility -a "agent"
# eg. utility -a claude

它做了这样几件事,启动扫描器,扫描项目符号,生成依赖图,并存储到 .utility/deps.sqlite 库中,然后启动 Agent,进行 Agent 终端,控制权回到 Agent,而 Utility.app 则在后台侦听文件变化并更新依赖图。

然后 Agent 可以通过本地 MCP 服务暴露的 project_search / project_structure / project_exports / project_packages 工具查询。

graph TB
    A[&#34;Agent&#34;] --> Q[&#34;查询层<br/>project_search · project_structure<br/>project_exports · project_packages&#34;]
    Q --> DB[&#34;.utility/deps.sqlite<br/>目录 · 文件 · 符号 · 包 · FTS&#34;]

    S[&#34;ProjectScanner<br/>全量扫描&#34;] --> E[&#34;符号提取&#34;]
    W[&#34;FileWatcher<br/>增量扫描&#34;] --> E

    E --> AST[&#34;AST 脚本<br/>Python · Babel&#34;]
    E --> RE[&#34;正则提取器<br/>Swift · JS · TS · Vue · Java · Kotlin&#34;]

    AST --> DB
    RE --> DB

知识面板,主动推送新鲜知识

这是关于个人成长的问题,我想解决它,问题的关键在于,Agent 用多了,主动阅读、探索新知识的意愿变低,我希望有一个主动弹出的面板,能展示一些我从未见过,或者说感兴趣的内容,能够让我花点时间去阅读。

为什么是从未见过的内容呢?我们知道,我们有一个记忆体,里面存了一些知识,为什么不能从里面提取一些内容来展示呢?

问题的关键在于,记忆体中的内容都是对话中产生的,这些内容已经在我们脑海中了,再次看到它不能让我们的大脑感到兴奋。所以我们需要一点未知的、感兴趣的新内容。

它的运作方式是这样的,我们会配置一些感兴趣的主题,然后大模型会去爬取有关主题的内容,当时间间隔到时,它会抽取一条知识显示在知识面板上。

graph TB
    U[&#34;用户&#34;] --> P[&#34;知识面板<br/>主题卡片 · 碎片&#34;]

    subgraph REFILL[&#34;后台补货&#34;]
        T[&#34;定时器&#34;] -->|&#34;池未满时补货&#34;| E[&#34;知识引擎&#34;]
        W[&#34;Web 检索&#34;] --> E
        E --> L[&#34;LLM 生成&#34;]
        L --> C[&#34;自检&#34;]
    end

    C -->|&#34;写卡&#34;| DB[&#34;知识池&#34;]
    DB -->|&#34;读取&#34;| P

这些核心能力在我的 Utility.app 中,我把它开源在:yuanyxh/utility