减少工具 token 操作

1. 背景

我们知道,工具的定义和调用结果很“占 token”,一个工具还好,但是我们会用到的工具一般都很多,而且有时候不知道是要用哪个,很难按需使用,导致光是工具的定义就可以撑包上下文,以及对数据格式的规范定义边界模糊。

但是 claude 为我们提供了 3 种解决思路(Tool Search Tool、Programmatic Tool Calling、Tool Use Examples)

2. Tool Search Tool

针对「工具定义 token 爆炸」、「按需使用」痛点,claude 提供了 Tool Search Tool 的方案:

  • 提供的工具箱中,用defer_loading: true,在上下文中隐藏部分工具(这些工具一般是按需使用的工具,并不需要时时刻刻都加载入上下文)
  • defer_loading: false:不需要隐藏的工具,也就是常用的小工具,可以一开始就载入上下文中

2.1. 如何让隐藏的工具按需载入?

工具箱中有一个小工具,它的功能是模糊匹配查找需要的工具。

这么说应该就比较懂了。

举例:claude 发现自己可能需要用到 github 相关的工具,那么他就会用这个查找小工具,查找 github ,小工具会去工具箱中找 github 关键词匹配的工具出来给 claude 使用。

3. Programmatic Tool Calling

针对「工具调用结果 token 爆炸」痛点,采取了以下方案:

  • Claude 通过编写脚本去编排和调用工具,并控制最终结果输出的内容格式。
  • Claude 只会看到了脚本处理后的结果,而不是完整的工具调用结果,抛弃了无用的信息,减少了 token
  • Claude 从始至终都是在进行沙盒操作

4. Tool Use Examples

针对「数据格式的规范定义边界模糊」的情况,claude 也有提供方案,不过在此之前,先解释一下,为什么要关心这个边界模糊问题。

4.1. 格式边界模糊会带来的问题

假设工具的响应结果中有时间,那么,这个是“2024-11-06”、“Nov 6, 2024”还是“2024-11-06T00:00:00Z”?

这种歧义可能会影响 claude 的脚本的中间处理过程,导致 claude 最终得到的结果出现偏差,并且由于脚本沙盒的原因, claude 无法确认这个结果是否发生偏差了。

4.2. 具体方案

以下是示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
{
"name": "create_ticket",
"input_schema": { /* same schema as above */ },
"input_examples": [
{
"title": "Login page returns 500 error",
"priority": "critical",
"labels": ["bug", "authentication", "production"],
"reporter": {
"id": "USR-12345",
"name": "Jane Smith",
"contact": {
"email": "jane@acme.com",
"phone": "+1-555-0123"
}
},
"due_date": "2024-11-06",
"escalation": {
"level": 2,
"notify_manager": true,
"sla_hours": 4
}
},
{
"title": "Add dark mode support",
"labels": ["feature-request", "ui"],
"reporter": {
"id": "USR-67890",
"name": "Alex Chen"
}
},
{
"title": "Update API documentation"
}
]
}

通过 input_examples可以让 claude 知道数据格式具体是什么样的

5. 并不是 3 种方案都必须严格执行

刚刚提到的 3 种方案并不代表一定要无时无刻都要实现,仅当你需要的时候才去采取这个方案。

  • 工具搜索工具的上下文膨胀 → Tool Search Tool
  • 大型中间结果污染上下文 → Programmatic Tool Calling
  • 参数错误和错误调用 → Tool Use Examples

不然你会很麻烦,比如Tool Use Examples,如果所有工具都要提供例子,那 token 一样会爆炸,仅仅当你需要举例让 claude 排除歧义的时候使用。

6. 参考文献

https://www.anthropic.com/engineering/advanced-tool-use

基于 AGUI 和 MCP 实现端 Tool

1. 概念解析

AGUI

是 Agent 与前端/客户端应用进行交互的一个通用通讯协议

MCP

是 LLM 远程调用 Tool 的一个通用协议

端 Tool

类似于 MCP 的 Stdio 版,就是用户端自行定义 Tool ,通过一定手段让远端的 Agent 可以调用用户端能力,包括远程操控浏览器等骚操作。

2. 实现方式

在远端 Agent 架起一个空 MCP Server ,而 MCP Server 内部是 AGUI ,当用户端连上远端 Agent 之后,MCP Server 通过 AGUI 协议向用户端发送 tool/list 和 tool/call 元数据,用户端通过 AGUI 协议返回对应的响应结果,当然这一环节的响应请求格式都是符合 AGUI 协议的,而非 MCP 协议。

当数据来到 MCP Server 时, MCP Server 将数据改造成 MCP 协议格式扔出去给 LLM 使用。

3. 流程示例如下

3.1. LLM 侧发起工具调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "agui_user_confirm",
"arguments": {
"title": "转账确认",
"body": "向 0x9c...b3ef 转 100 USDT?",
"ok_text": "确认转账",
"cancel_text": "再想想"
}
}
}

3.2. agui-mcp-server 收到后 → 把参数包装成 AG-UI 事件

AGUI 事件名【agui.tool.invoke】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /agui/events?id=3
Content-Type: text/event-stream

event: agui.tool.invoke
data: {
"call_id": "3", // 与 MCP jsonrpc id 保持一致
"tool": "agui_user_confirm",
"params": {
"title": "转账确认",
"body": "向 0x9c...b3ef 转 100 USDT?",
"okText": "确认转账",
"cancelText": "再想想"
}
}

3.3. 端侧(Web)收到事件 → 渲染弹窗

AGUI 事件名【agui.tool.return】

用户点击 “确认转账” 后,前端把结果写回:

1
2
3
4
5
6
WebSocket SEND:
{
"type": "agui.tool.return",
"call_id": "3",
"result": "ok"
}

3.4. agui-mcp-server 把前端回包转成 MCP CallToolResult

1
2
3
4
5
6
7
8
9
10
11
12
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "ok"
}
]
}
}

然后交付给 LLM

End.

coze loop

1. 背景

跟着团队去开发 AI 应用时,经常会遇到如下情景:

  • Prompt 一改,回答跑偏,纯黑盒,只能回滚;
  • 用户投诉“变慢了”,却找不到是哪一次模型调用拖了后腿;
  • 老板问“新模型效果真的更好吗?” 额,不知道。

直到把 Coze-Loop 加入到 Agent 中,终于实现了“像观测微服务一样观测 Agent”。

2. 是什么

一句话:字节 2025.7 开源的 AI Agent 生命周期管理平台,定位等价于“LLM 时代的 Prometheus + Grafana + JMeter”。

模块 作用 类比传统中间件
Prompt Playground 在线调试、版本 diff Postman
评测中心 自动打分、回归测试 JMeter
全链路 Trace 每一次模型调用可追踪 Jaeger
实验管理 A/B 切流、显著性检验 Flagr
监控大盘 Token 延迟、异常率 Grafana

3. 核心能力拆解

  1. Prompt Playground:把“玄学”变“工程”

支持 多模型并行对比(OpenAI/Ark/千帆/通义/Gemini/Claude),一个输入六份输出,差异一眼可见 ;

每次调试自动生成 语义哈希,相同 Prompt 只存一份快照,节省 60% 空间;

一键“版本冻结”→ 生成 /prompt/v1.3.0 这样的只读端点,供线上服务通过 SDK 热加载,彻底告别“改完就回滚”。

  1. 系统化评测:让老板相信“新模型更好”

数据集管理:支持 CSV、JSONL、飞书多维表,自动采样、去重、敏感词过滤;

评估器工厂:内置 BLEU、ROUGE、BERTScore、LLM-as-Judge(用更强的模型给弱模型打分);

实验报告:自动计算↑↓变化率、p-value,红绿指标一键导出 PPT,老板再也不拍脑袋。

  1. 全链路 Trace:一次对话,一张瀑布图
  • 自动解析 Prompt → 模型 → 工具 → 结果 四级跨度,瀑布图可直接下到 SQL;
  • 与现有 Grafana 无缝打通,Token 延迟、异常率、限流次数 三合一大盘 。

本地事件总线

在单体服务或边缘组件中,使用“本地事件总线”可以将核心交易流程与非关键但必要的“旁路逻辑”(如审计、埋点、异步落库等)解耦,同时在需要时以异步方式削峰。本文给出一个工程化的最小可用实现与实践要点。

适用场景

  • 非关键路径的“扩展逻辑”:埋点、通知、审计、缓存刷新、异步刷库等。
  • 局部解耦:不引入外部 MQ 的情况下,让发布方不直接依赖消费者实现。
  • 小成本削峰:在请求线程外异步执行,减少主流程时延。

不适合的场景:需要跨进程/跨服务可靠投递、严格有序性、可回溯性或高可用的强需求(此类应考虑 Kafka/RabbitMQ/事件流平台)。

设计目标

  • 简洁 API:注册、发布、选择同步/异步。
  • 并发安全:注册/发布并发可用。
  • 隔离性:消费者异常不影响发布方主流程。
  • 可观测:最少限度的错误暴露与日志挂接点。

核心实现(线程安全、可选异步)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package eventbus

import (
"context"
"log"
"sync"
)

// 事件处理函数签名:按需自定义参数结构体
type HandlerFunc func(ctx context.Context, arg1, arg2 MyStruct)

type Bus struct {
mu sync.RWMutex
handlers map[string]HandlerFunc
}

func New() *Bus {
return &Bus{handlers: make(map[string]HandlerFunc)}
}

// Register 注册事件处理器;重复注册将覆盖旧处理器
func (b *Bus) Register(name string, h HandlerFunc) {
b.mu.Lock()
b.handlers[name] = h
b.mu.Unlock()
}

// Publish 发布事件;async=true 时在 goroutine 中执行
// 未注册的事件将返回 false 便于上层打点
func (b *Bus) Publish(ctx context.Context, name string, arg1, arg2 MyStruct, async bool) bool {
b.mu.RLock()
h, ok := b.handlers[name]
b.mu.RUnlock()
if !ok {
return false
}

run := func() {
// 防御:隔离消费者 panic,避免影响主流程
defer func() {
if r := recover(); r != nil {
log.Printf("eventbus handler panic: name=%s, recover=%v", name, r)
}
}()
h(ctx, arg1, arg2)
}

if async {
go run()
} else {
run()
}
return true
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 初始化总线
var bus = eventbus.New()

// 注册:例如“异步刷盘 MySQL”
bus.Register("async_flush_mysql", func(ctx context.Context, arg1, arg2 MyStruct) {
// 执行刷盘动作
})

// 发布:同步
ok := bus.Publish(ctx, "async_flush_mysql", a1, a2, false)
if !ok {
// 可记录未注册事件的观测日志
}

// 发布:异步
_ = bus.Publish(ctx, "async_flush_mysql", a1, a2, true)

工程实践要点

  • 错误与可观测:
    • 事件未注册时返回值用于统计/告警。
    • 建议在处理器内统一接入日志/指标埋点,并结合 context 传递 traceID。
  • 上下文与超时:
    • 异步处理应尊重 ctx,在耗时操作处检查取消/超时,避免后台 goroutine 漫游。
  • 并发与容量:
    • 如果异步任务可能堆积,增加限流/队列(如带缓冲 channel + worker 池)。
    • 长尾、重试、退避策略应在处理器内自行控制。
  • Panic 隔离:
    • 已在 Publish 中加了 recover,仍建议处理器内做细粒度防御。

与消息队列的对比

  • 本地事件总线:轻量、零依赖、进程内调用,可靠性受制于进程生命周期,不具备跨服务投递保障。
  • 专业 MQ/事件流:具备持久化、回溯、重放、消费组、顺序与扩展性;成本与复杂度更高。

小结

本地事件总线适合在单体或简易服务中对旁路逻辑进行解耦与异步化处理。通过线程安全的注册/发布、panic 隔离与必要的可观测性,即可以极低成本获得显著的工程收益;当需求升级到跨服务可靠投递与大规模扩展时,再自然过渡到专业消息系统。

AI 上下文工程

长期记忆系统

反思

  • 元提示(框架):
    • 向 llm 提问,让其回顾失败,生成摘要避免未来犯错

观察

  • 原始日志收集
  • 合成日志
    • 让 llm 总结出新洞察

选择

  • 工具集
    • 使用 RAG 建立工具索引,这样子可以把最相关的 n 个加载进上下文而无需全部加载
  • 固定+遮蔽:
    • 通过复用 cv 缓存来加速
  • 混合检索
    • 多路召回:向量、关键词、知识图谱,多方面去获取信息
    • Agentic 探索: agent 在循环中,主动使用检索工具进行多步推理
    • 重排序:把召回的数据进行二次总结,用更加精细的规则或模型去提高信噪比

压缩

  • 上下文总结
    • LLM 提炼
      • 总结对话历史(提炼成任务概览)
      • 对海量的 API 返回结果总结成摘要
  • 上下文裁剪
    • 丢弃信息
      • 滑动窗口(只保留最近n轮对话)
      • 用轻量级的模型进行智能裁剪

微调 vs 上下文工程

  • Manus 团队优先上下文工程发展

kv 缓存设计

  • kv 缓存工作机制
    • 前缀匹配
  • 提高缓存命中率的工程原则
    • 选用 vllm 这种能支持 kv 缓存功能的现代推理框架
    • 缓存只存于同一个推理进程,所以需要用唯一标识来确保同一个会话请求打到同一个进程
    • 保持前缀稳定
    • 上下文只追加,不修改或删除中间部分
    • 如果不支持自动前缀匹配的框架,需要手动插入断电来进行缓存

工具选择

响应预填充

通过特定的前缀标签,为模型提前提供模式信号,引导他提前激活相应的模式

img

专注力

todo.md

  • 通过 write 工具在两个关键时机更新 todo.md
    • 复杂任务分解后写入
    • 完成每个子任务就覆写最新进度

反直觉

将错误日志保留在上下文中,降低其重复犯错可能

多 Agent 协作

  • 通信
    • 上下文不共享,子 agent 只处理原子性子任务
  • 共享上下文
    • 类似于 fork
    • 子 agent 拥有主 agent 全部历史+子 agent 新 system prompt

三层工具

  • 极少数、固定的原子函数
  • 能力扩展,通过第一层的原子函数(execute_shell)调用获得外部动态工具箱
  • 组合使用第一层和第二层编写并执行脚本

处理大型工具输出

  • 创建子 agent 去提炼,只返回固定的、结构化的结果。(就是一层层套娃,直到套娃到原子行为)

mcp 协议理解

  1. Mcp 协议分析

整个 Mcp ,无论是 Stdio 、 Sse 、Streamable ,只要遵守对应的 JSONRPC 请求和响应格式,即使你实际上只是一个空壳,大模型依然会相信你是个 Mcp 。

  1. Stdio

Stdio 实际上是本地执行命令。

  • 举个例子,假设你本地有一个 exe 文件,路径是:C://User/test.exe (路径我乱编的,格式可能有一点点错误,补药太在意哈),那么你的 Stdio Mcp 的 Command 选项就可以填 C://User/test.exe ,至于 Args 这个看你这个 exe 要不要,其实就是 -h 那些,而 Env 这个就是环境变量,配 C 语言环境的那个环境。
  • 为什么 Cherry Stdio 使用不需要我自己配环境?我没有深究过,但是基本可以确定 Cherry Stdio 提供了一个会话,这个会话中的环境变量被 Cherry Stdio 配过,所以只影响当前会话,而不会永久影响你的电脑环境配置。
  • 整个过程
    • 大模型通知 Cherry Stdio 我要用这个 Mcp ,包括对应的参数什么的
    • Cherry Stdio 收到消息,执行 exe 并在与 exe 的标准输入中输入 JSONRPC 消息
    • Exe 从标准输入流中获取信息并处理后返回一个 JSONRPC 消息,输出到标准输出里
    • Cherry Stdio 拿到数据,喂给大模型
    • 大模型根据新消息再吐出最终结果

参考的 JSONRPC 请求和响应数据

1
2
3
req: {"jsonrpc":"2.0","method":"initialize","id":0,"params":{"capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"mcp-inspector","version":"0.14.0"},"protocolVersion":"2025-03-26"}}

res: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"markitdown","version":"1.8.1"}}}
  1. Sse

Sse 骚操作有点多,他输入和输出不是同一个接口,也就是说他由两个接口组成一个完整的输入输出,类似下面

img

  • sse 端点:
    • 用于向客户端发消息,这个端点接口会持续存在,符合 sse 协议,持续吐消息
  • messages
    • 用于接收客户端的消息,一次请求为一次接收,消息处理的结果会从 sse 端点那里吐出
  • 整个过程
    • 请求 sse 端点, sse 端点会返回一个 endpoint 消息,里面包含了 messages 端点路径和分配的 sessionid ,至于前面的域名什么的,约定与 sse 端点的域名保持一致(http://127.0.0.1:8094)
    • img
    • 向 messages 端点发送初始化消息,messages 端点只会响应一个 accepted
    • img
    • img
    • 此时 sse 端点会得到 server 返回的信息
    • img
    • 包括 tool call 也是一样的
    • img
    • img
  1. Streamable

比起 sse 好一点,没那么精神分裂,非要搞两个接口,一个接口就完事了,实际上我个人感觉跟普通的 http 接口没啥区别,也就多了一些约定

约定

  • 请求
    • Header
      • Mcp-Session-Id sessionid 存在这里,用于告诉服务端听复用之前的信息
      • Accept 默认 application/json, text/event-stream
  • 响应
    • Header
      • Mcp-Session-Id 把 服务端分配的 sessionid 交给客户端

其他跟正常 http 请求没区别,只是请求响应 json 格式有固定要求而已

1
2
3
req: {"jsonrpc":"2.0","method":"initialize","id":0,"params":{"capabilities":{"roots":{"listChanged":true},"sampling":{}},"clientInfo":{"name":"mcp-inspector","version":"0.14.0"},"protocolVersion":"2025-03-26"}}

res: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"markitdown","version":"1.8.1"}}}
  • 过程展示
    • 这里就不展示了,就是一次请求一次响应信息,首次请求会在 header 中返回 sessionid ,之后的每次请求都带上这个 sessionid 以此保持会话状态。
    • 下面这个是我的 streamable 接口处理的一小部分代码,已经可以说明上面的约定了

img

img

  1. Mcp 请求响应分析

众所周知, Mcp 的请求响应都遵守了一个约定,字段名什么的都约定好了。

  1. 请求

这里可以看出来,以下约定:

  • 必须携带 jsonrpc 字段,一般是 2.0
  • method :根据实际想干嘛来提供对应的 method
  • id :每次请求叠加 1 ,初始为 0
  • params :根据 method 的不同也不同,对应 go 中的 interface{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 初始化
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 0,
"params": {
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "mcp-inspector",
"version": "0.14.0"
},
"protocolVersion": "2025-03-26"
}
}


// tool call
{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"_meta": {
"progressToken": 3
},
"arguments": {
"uri": "https://aitoken-public.qnaigc.com/test/transformer.pdf"
},
"name": "convert_to_markdown"
}
}

// tool list
{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 4,
"params": {
"_meta": {
"progressToken": 4
}
}
}
  1. 响应

约定

  • 必须携带 jsonrpc 字段,一般是 2.0
  • id :请求给啥返回啥
  • result: 客户端真正想要的数据,一样是 interface{}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 初始化
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"serverInfo": {
"name": "markitdown",
"version": "1.8.1"
}
}
}


// tool call
{
"jsonrpc":"2.0",
"id":3,
"result":{
"content":[
{
"type":"text",
"text":"。。。。"
}
]
}
}

// tool list
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"tools": [
{
"annotations": {},
"description": "Convert a resource described by an http:, https:, file: or data: URI to markdown",
"inputSchema": {
"properties": {
"uri": {
"title": "Uri",
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"name": "convert_to_markdown"
}
]
}
}
  1. Method

这个好像很全:https://modelcontextprotocol.info/zh-cn/

每个 method 都有对应的确认的 params 格式和 result 格式,可以看我下面提供的例子里看他的 params 和 result 长啥样。

目前我知道的 method 分为了以下几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const (
Initialize = "initialize"
Ping = "ping"
ResourcesList = "resources/list"
ResourcesTemplatesList = "resources/templates/list"
ResourcesRead = "resources/read"
ResourcesSubscribe = "resources/subscribe"
ResourcesUnsubscribe = "resources/unsubscribe"
PromptsList = "prompts/list"
PromptsGet = "prompts/get"
ToolsList = "tools/list"
ToolsCall = "tools/call"
LoggingSetLevel = "logging/setLevel"
CompletionComplete = "completion/complete"
NotificationsInitialized = "notifications/initialized"
)
  1. initialize

在客户端正式启动之前,需要一个激活操作,也就是客户端往服务端发送 initialize 消息,告知服务端,客户端的身份,服务端会分配一个 sessionid 给客户端,之后的聊天就依据 sessionid 知道你是谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
req: {
"jsonrpc": "2.0",
"method": "initialize",
"id": 0,
"params": {
"capabilities": {
"roots": {
"listChanged": true
},
"sampling": {}
},
"clientInfo": {
"name": "mcp-inspector",
"version": "0.14.0"
},
"protocolVersion": "2025-03-26"
}
}

res: {
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {
"prompts": {},
"resources": {},
"tools": {}
},
"serverInfo": {
"name": "markitdown",
"version": "1.8.1"
}
}
}
  1. ping

就是 ping 一下看你还活不活着

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
req: {
"jsonrpc": "2.0",
"method": "ping",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
  1. resources/list

不知道能干啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "resources/list",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"resources": []
}
}
  1. resources/templates/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "resources/templates/list",
"id": 2,
"params": {
"_meta": {
"progressToken": 2
}
}
}

res: {
"jsonrpc": "2.0",
"id": 2,
"result": {
"resourceTemplates": []
}
}
  1. resources/read

没用过

  1. resources/subscribe

没用过

  1. resources/unsubscribe

没用过

  1. prompts/list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
req: {
"jsonrpc": "2.0",
"method": "prompts/list",
"id": 1,
"params": {
"_meta": {
"progressToken": 1
}
}
}

res: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"prompts": []
}
}
  1. prompts/get

没用过

  1. tools/list

列出工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
req:{
"jsonrpc": "2.0",
"method": "tools/list",
"id": 4,
"params": {
"_meta": {
"progressToken": 4
}
}
}





res:{
"jsonrpc": "2.0",
"id": 4,
"result": {
"tools": [
{
"annotations": {},
"description": "Convert a resource described by an http:, https:, file: or data: URI to markdown",
"inputSchema": {
"properties": {
"uri": {
"title": "Uri",
"type": "string"
}
},
"required": [
"uri"
],
"type": "object"
},
"name": "convert_to_markdown"
}
]
}
}
  1. tools/call

执行工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
req:{
"jsonrpc": "2.0",
"method": "tools/call",
"id": 3,
"params": {
"_meta": {
"progressToken": 3
},
"arguments": {
"uri": "https://aitoken-public.qnaigc.com/test/transformer.pdf"
},
"name": "convert_to_markdown"
}
}


res:{
"jsonrpc":"2.0",
"id":3,
"result":{
"content":[
{
"type":"text",
"text":"。。。。"
}
]
}
}
  1. logging/setLevel

没用过

  1. completion/complete

没用过

  1. notifications/initialized

这个有点东西,动不动发一个的,但是你不用回复

  1. Mcp 与 Eino

Eino

是字节研发的一款大模型二开框架

Eino 是怎么用 Mcp 的(参考 react agent)

  • 对于 Eino 而言, Mcp 被视为了一个 Tool ,所以通过 Eino 让大模型调用 Mcp 就变成了:
    • Eino 告诉大模型有这些 Tool
    • 大模型通过 tool_calls 通知 Eino 调用对应的 Mcp
    • Eino 将 Mcp 调用结果重新“投喂”给大模型
    • 大模型根据上下文再说出最终的结果

如何将 Eino 隐藏起来的对话消耗 Token 拿到手

从上面描述可以得知,Eino 最终吐出的消息在背地里是经过了 n 次对话产生的

  1. 学习资料

  • mcp (server or client) go 版:

https://github.com/mark3labs/mcp-go

  • mcp 流量转发:

https://github.com/kkb-1/mcprouter

  • mcp 调试工具( F12 看他的网络请求,了解 mcp 实际协议干了啥):

https://mcp-docs.cn/docs/tools/inspector

  • mcp proxy(协议转换,例如 stdio 转 sse ,这个会用就行):

https://github.com/sparfenyuk/mcp-proxy

  • eino (字节的 cloudwego 系列,大模型二开框架):

https://www.cloudwego.io/zh/docs/eino/ecosystem_integration/tool/tool_mcp/

如何快速上手一个项目(后端)

  1. 要项目拉取权限
    这一点虽然很废话,但是你刚来就敢主动找 mt 要项目不?
  2. 有没有开发规范和接口文档?
    问 mt 要一个看,如果没有那也没事,有的话会更方便你后续开发
  3. 寻找 main
  • main 是运行代码的入口,只有找到了它,你才能开始后续的操作
  • 一般来说 main 中会包含:
    • 配置初始化
    • 定时任务启动(当然,一般一个已经稳定的项目,它的定时任务是分离出去的)
    • 创建 web 服务器
  1. 快速熟悉项目代码风格
  • 先寻找 router ,在 router 中寻找一个你认为分离得比较成功的路由小模块
  • 看路由写法
  • 随机找一个接口进去看
  • 如果你有开发规范文档,那么就可以结合着文档了解他们的接口名、函数名、变量名、结构体名这些命名规范
  • 看它是怎么使用 log 、 db 和 cache 的
  • 如何处理错误,错误应该如何构建
  • 常量是放在哪里的?
  • 好了,现在回过头来看一下,你一路追下来的文件的文件夹层是怎么个分布法(恭喜,你已经会写一个符合团队风格的接口了)
  • 寻找 config ,你只有一个任务,它是怎么读配置的
  1. 同步业务信息
  • 直接看 git 提交记录,等同于爬楼看聊天记录一样,如果前者的 commit 写得正常点,你就能快速看懂它在干嘛

第三方支付接入(stripe为例)

  1. 开启交易会话
    1. 使用平台提供的私钥,向平台的创建交易会话的接口发起请求,生成一个交易链接,需要提供价格ID、商品数量、成功支付重定向链接、取消支付重定向链接等自定义参数
    2. 价格ID:价格在stripe中是一个概念实体,比如你有一个VIP服务商品,可以一次性开通一个月,也可以连续包月,那这个商品就有两个价格了,每个价格的单价与结算方式是不一样的,同时每个价格都有唯一的ID,即价格ID
  2. 事件监听
    【背景】用户的支付是在第三方平台完成的,对于我们服务端来说是无感知的,即你无法直接得知用户是否付完钱、哪个用户付了哪一笔钱。所以你需要向第三方平台订阅这个消息,这个订阅也叫做事件监听
    1. 创建服务端事件回调接口:当第三方平台想要向服务端发送消息时,就会把消息发送到事件回调接口,事件回调接口接收消息并正确处理消息(包括:将订单状态转为成功支付)
    2. 注册第三方事件监听:在第三方注册自己的回调接口以及需要触发回调的事件类型
    3. 事件成功监听:第三方与服务端会约定一个规则来确认是否服务端成功接收并处理事件,例如服务端成功接收并处理事件就返回200状态码
    4. 事件重发行为:由于网络环境复杂,不能确保发一次消息就能成功,所以一般第三方会有不断重发的行为逻辑,知道得到服务端的确认(例如:200状态码)
    5. 事件监听的不可信:不论是网络问题还是第三方平台服务崩溃问题,都有可能导致消息发送失败,并且支付可能有延迟完成的情况(比如银行走流程确认转帐什么的),所以第三方支付一般不保证事件实时发送给服务端
    6. 事件鉴权:与第三方约定好一个鉴权机制,确保请求事件监听回调接口的是第三方
  3. 最终方案
    1. 发起交易会话的同时,生成一个订单ID,与会话ID结合加密生成一个新ID
    2. 支付成功重定向的网址拿会话ID请求事件回调接口,事件回调接口发现无法通过strip的鉴权,认为是重定向的请求,此时向strip发起请求确认交易会话状态
    3. 为了防止支付成功并重定向那一刻服务端崩溃,以事件监听作为兜底机制
    4. strip请求事件回调接口,通过strip的鉴权,可以信任strip发来的事件状态,也可以不信任,重新向strip发起请求确认交易会话状态
    5. 总体采用状态机模式
    6. 确认订单状态,完成订单

docker 文件系统

联合文件系统

联合文件系统(Union File System)UnionFS是docker制作镜像和容器文件的主要技术。通过联合文件系统,可以将多个路径挂载到同一个挂载点上,实现多个path的合并操作(上层同名文件会将下层文件覆盖),最后通过挂载点想上层应用/用户呈现一个合并之后的视图,联合文件系统有多种实现,ubuntu采用AUFS实现,centos采用overlay/overlay2来实现。

docker pull 断点续传

  • 随着技术和业务不断迭代,镜像层不断叠加,会出现非常大的镜像,以默认的docker配置拉取的话,网络一旦波动就会前功尽弃,这其实是非常灾难性的情况
  • docker可以通过在配置文件中添加👇的配置内容,重启docker后就可以支持断点续传
1
2
3
 "features": {
"containerd-snapshotter": true
}
  • 断点续传配置生效后,docker原有的镜像将无法识别,也就是假性清空,可以把断点续传配置取消,镜像就又能识别了

原理

  • docker断点续传本质是不断对拉取的镜像层打“快照”,如果你有一次突然中断了拉取,再次进行拉取时,docker会取出快照,接着原来的进度继续拉取,所以断点续传会产生较多的冗余数据,占空间