“写笔记”支持四种格式——Word 文档、Excel 表格、Markdown、纯文本,起稿或二次编辑时都能随时切换,同一篇笔记想用哪种形态来记,都由你说了算。
md、txt、csv、json 这类纯文本则原样载入,不做多余加工。拿一张现成的表倒进来、改几笔、再导出去,等于白用一台免费的格式转换器。
要带走就在右上角点“下载”,可导出 PDF、Word、Markdown、Excel、TXT 等格式;列表卡片“⋯”菜单里,也有同样的下载入口。
在“工具”页点“+ 上传工具”即可发布:填好名称与链接,再用 Markdown 把使用方法写清楚——能解决什么问题、怎么装、怎么用,比堆介绍实在。
要分发安装包就一并上传压缩包(ZIP、RAR、7Z、TAR.GZ,最大 35MB),别人在详情页一键下载;只放链接不带附件也可以。
工具按大家的收藏热度排序,好用的自然会被顶上来。发布后可在详情页或卡片菜单里编辑、下架。
写笔记时勾上“隐藏”,这篇就只存在于你自己的账号里:不进列表、不进搜索、不上首页精选,也不会出现在任何公开的页面,链接发给别人同样打不开。
适合放密码、草稿、日记这类只给自己看的内容;想公开,去“发布”打开它,把“隐藏”的勾去掉再保存,之后编辑会默认保持原状态,不会悄悄变回公开。
你的内容会同时保存在多个副本上,系统定期做备份与完整性校验,再配合异地容灾机制:就算某台机器出问题,数据也不会丢,可以长期放心存放;特别重要的资料,仍建议你另外再留一份备份。
全站跑在容器化、模块化的现代架构上,更新、部署、回滚都很快,扩展性和稳定性都按长期运营的标准来设计(Built for reliability, designed to scale)。
这个网站最早只是一个人的笔记仓库,后来慢慢长成现在的知识中枢。设计上很克制——没有广告、没有追踪、没有推荐算法,只是干干净净地存放一些东西;既然做好了,就公开出来,万一有人用得上呢。
不做大而全,不做平台梦,保持简单、保持克制、保持好奇。所有内容都由用户贡献、由用户维护:不会突然冒出付费墙,不会在角落塞广告位,也不会把你的数据卖给第三方。
产品会持续迭代,站内日志页记录着每一次改动,改了什么都有迹可循;想了解这个站是怎么一步步走到今天的,翻翻日志就能看到来龙去脉。
如果在这里看到涉嫌违规的内容,点对应卡片右侧的“举报”按钮就能提交,我们会尽快核实处理;也谢谢你花一点时间,一起把这里维护干净。
NodeLLM 1.17:MCP采样、并发工具执行与更智能的ORM控制
NodeLLM 1.17 深度技术笔记:MCP采样、并发工具执行与ORM控制升级
前言
兄弟们,NodeLLM 1.17来了。这次更新其实挺有意思的——两个核心功能,一个是之前挖的坑终于填上了,另一个是社区喊了很久的优化。让我一个一个掰开来讲。
先说MCP采样这个事。如果你还记得我们之前引入MCP支持时埋的伏笔,当时我说了三个阶段:第一阶段是让服务器暴露工具和资源给客户端,第二阶段是完善协议交互,第三阶段就是采样(Sampling)。采样这个东西,说白了就是把MCP的通信方向反过来——不是客户端问服务器要工具,而是服务器问客户端帮忙跑一次LLM补全。这样一来,MCP服务器就能提供LLM驱动的能力,比如摘要、分类、草稿生成,而不用自己搞API key或者对接模型提供商。
MCP采样:闭环完成
原理背景
先理解一下为什么需要采样。在标准的MCP架构里,客户端是主动方,它发现服务器提供的工具和资源,然后自己调用LLM来决定用哪个工具、传什么参数。但有些场景下,服务器本身需要LLM的能力来完成某个任务——比如一个代码分析服务器,它可能想调用LLM来生成代码注释或者解释一段逻辑。如果没有采样,服务器就得自己去对接一个LLM服务,这就破坏了MCP的“服务器只管提供能力,客户端管LLM”的设计初衷。
采样的核心价值在于:服务器可以安全地委托LLM调用给客户端。客户端有自己的模型配置、API key、安全策略,服务器不用操心这些。服务器只需要说“嘿,帮我跑一下这个补全请求”,客户端用自己的模型跑完,把结果返回给服务器。
具体实现
NodeLLM 1.17通过createLLMSamplingHandler来实现这个机制。看代码:
import { createLLM } from "@node-llm/core";
import { MCP, createLLMSamplingHandler } from "@node-llm/mcp";
const llm = createLLM({
provider: "openai"
});
const mcp = await MCP.connect(
{
command: "node",
args: ["./sampling-server.mjs"]
},
{
sampling: createLLMSamplingHandler(llm, "gpt-4o-mini")
}
);
const tools = await mcp.discoverTools();
这里有几个关键点要注意:
握手阶段声明采样支持:服务器只有在握手阶段发现客户端声明了采样支持,才会暴露那些依赖采样的工具。这是个安全设计——如果客户端不支持采样,服务器就不该展示那些需要采样才能工作的工具,否则工具调用会失败。
模型选择:
createLLMSamplingHandler的第二个参数指定了模型。这里用的是gpt-4o-mini,但你可以改成任何NodeLLM支持的模型。这意味着服务器端的工具能力完全由你客户端的模型配置决定。底层原理:当服务器发起一个
sampling/createMessage请求时,createLLMSamplingHandler会接管这个请求,把它转发给NodeLLM实例。NodeLLM用你配置的模型和提供商来生成补全,然后把结果包装成CreateMessageResult返回给服务器。
高级用法:自定义采样处理
如果你需要更精细的控制——比如根据模型提示(model hint)路由到不同的模型,或者注入你自己的安全护栏——可以传一个普通函数而不是{llm, model}对象:
const mcp = await MCP.connect(
{ command: "node", args: ["./sampling-server.mjs"] },
{
sampling: async (params) => {
// params 是原始的 sampling/createMessage 参数
// 你可以在这里做任何处理
// 比如根据模型提示路由
if (params.modelPreference?.hints?.includes("fast")) {
return await fastModelHandler(params);
}
// 比如注入安全检查
const sanitized = await sanitizeInput(params);
// 比如拒绝某些请求
if (isBlocked(params)) {
return { error: "Request blocked by policy" };
}
// 最终返回 CreateMessageResult
return await myCustomHandler(params);
}
}
);
这个设计的好处是:你可以完全控制采样的行为,包括是否响应、如何响应、用什么模型响应。对于企业级应用来说,这意味着你可以在客户端统一管理所有的LLM调用策略,服务器端完全不需要关心这些。
并发工具执行:告别串行等待
问题背景
这是社区反馈了很久的一个痛点。当一个模型在一次响应中返回多个独立的工具调用时,NodeLLM之前一直是串行执行的。举个例子:
用户问:“东京、伦敦、纽约的天气怎么样?”
模型可能会返回三个get_weather调用,参数分别是三个城市。在旧版本中,这三个调用会一个接一个地执行——先查东京,等结果回来再查伦敦,再等结果回来再查纽约。如果每个API调用需要500ms,那总耗时就是1.5秒。
但问题是,这三个调用完全没有依赖关系。查东京天气不需要等伦敦的结果。串行执行纯粹是在浪费时间。
解决方案
toolConcurrency选项就是为了解决这个问题。它让并执行变成可选的:
const chat = llm
.chat("gpt-4o-mini")
.withTool(WeatherTool)
.withToolConcurrency(true);
await chat.ask("What is the weather in Tokyo, London, and New York?");
加上withToolConcurrency(true)之后,这三个天气查询会同时发起,总耗时从1.5秒降到500ms。效果立竿见影。
使用场景和注意事项
这个功能在stream()和Agent模式下同样有效:
// 流式模式
const stream = llm
.chat("gpt-4o-mini")
.withTool(WeatherTool)
.withToolConcurrency(true)
.stream("查询三个城市的天气");
// Agent模式
const agent = llm
.agent("gpt-4o-mini")
.withTool(SearchTool)
.withToolConcurrency(true);
但要注意:并发不是万能的。有些工具调用是有依赖关系的——比如先搜索用户信息,再根据用户信息发送邮件。这种情况下就不能用并发。所以toolConcurrency默认是false,需要你明确知道调用之间没有依赖关系时才开启。
另外,并发执行对工具的幂等性有要求。如果同一个工具被并发调用多次,每次调用应该独立且不会产生副作用冲突。比如“创建订单”这种工具就不适合并发,因为多个并发调用可能导致资源竞争。
回调函数叠加:不再覆盖
问题
这是一个很隐蔽但很坑的问题。以前如果你写:
chat.onEndMessage((msg) => audit.log(msg));
chat.onEndMessage(() => ui.refresh());
第二个onEndMessage调用会静默覆盖第一个。第一个审计日志回调就没了,而且没有任何警告。这在大型项目中特别容易出问题——比如你在模块A注册了日志回调,在模块B注册了UI更新回调,两个模块互不知道对方的存在,结果模块B的注册把模块A的顶掉了。
解决方案
现在所有回调都是叠加的,按注册顺序执行:
chat
.onEndMessage((msg) => audit.log(msg))
.onEndMessage(() => ui.refresh());
// 现在两个回调都会执行,先审计日志,再刷新UI
chat
.beforeRequest(redactPII)
.beforeRequest(logOutboundPrompt);
// 请求前先脱敏PII,再记录请求日志
这个改动影响所有on*()、beforeRequest()、afterResponse()钩子。对只有一个回调的常见情况来说,行为完全没有变化。只有当你从多个地方注册回调时,才会感受到区别。
设计思路
这个改动的本质是把回调注册从“单赋值”变成了“数组追加”。你可以把它想象成中间件(middleware)模式——每个回调都是处理链中的一个环节,按照注册顺序依次执行。这让组合多个独立关注点变得安全且可预测。
ORM 0.8.0:工具控制持久化
为什么需要这个
@node-llm/orm是NodeLLM的持久化层,基于Prisma。以前如果你想对工具执行做精细控制——比如人工确认、错误处理、工具选择——你得降到core API层面去操作。这就导致一个问题:你在ORM层保存的聊天记录,和你在core层实际执行的行为,可能不一致。
新特性
ORM 0.8.0现在暴露了和core相同的工具执行控制接口:
import { createChat } from "@node-llm/orm/prisma";
import { ToolExecutionMode } from "@node-llm/core";
const chat = await createChat(prisma, {
model: "gpt-4o"
})
.withToolExecution(ToolExecutionMode.CONFIRM)
.onConfirmToolCall(async (call) => await askUserToApprove(call))
.onToolCallError((call, error) => ({ error: error.message }))
.withToolChoice("get_weather")
.withToolConcurrency(true);
三种执行模式
toolExecution接受三种模式:
auto(默认):自动执行所有工具调用。和以前的行为一样。
confirm:每次执行工具前调用
onConfirmToolCall回调。这适合“人工在环”(human-in-the-loop)的场景——比如用户要调用一个写数据库的工具,你希望先弹个确认框让用户批准。dry-run:跳过所有工具执行。这个模式在测试和调试时特别有用——你可以看到模型打算调用什么工具、传什么参数,但不会真的执行。
组合使用
这些控制选项可以组合使用。比如你可以同时设置:
withToolExecution(ToolExecutionMode.CONFIRM):需要人工确认withToolChoice("get_weather"):强制模型只能调用天气工具withToolConcurrency(true):允许并发执行
而且所有模型实际执行的操作都会被正确持久化到数据库里。这意味着你可以随时回溯聊天记录,看到每个工具调用是什么时候执行的、用了什么参数、返回了什么结果。
Monitor更新:更精细的Token计数
这次还顺便更新了@node-llm/monitor和@node-llm/monitor-otel,主要改进是更细粒度的Token分类。
问题
以前Token计数只分prompt和completion两类。但现在LLM的Token使用越来越复杂:
- 缓存Token(cached):来自prompt缓存的Token,不计入实际消耗
- 缓存创建Token(cacheCreation):创建缓存时消耗的Token
- 推理Token(reasoning):模型内部推理过程消耗的Token,比如Chain-of-Thought
- 图像Token(image):多模态输入中的图像Token
不同提供商对这些Token的命名完全不同。Vercel AI SDK用cachedInputTokens,OpenAI用snake_case,OTel GenAI语义约定用cache_read_input_tokens。以前要手动处理这些差异,非常痛苦。
解决方案
现在所有Token使用都会被归一化为统一的接口:
interface ExtractedTokenUsage {
prompt: number; // 提示Token
completion: number; // 补全Token
cached: number; // 缓存命中Token
cacheCreation: number; // 缓存创建Token
reasoning: number; // 推理Token
image: number; // 图像Token
}
不管原始事件用什么命名规范,Monitor都会提取出这六个数字。这样你的仪表盘和时间序列聚合就能看到一致的Token使用情况,成本和用量分析也更加准确。
实际意义
随着推理模型(比如GPT-4的推理模式)和prompt缓存成为默认配置而不是例外情况,这种细粒度的Token分类变得越来越重要。比如:
- 如果你大量使用prompt缓存,
cached和cacheCreation的比值可以帮你评估缓存命中率 - 如果你用推理模型,
reasoning和completion的比值可以帮你了解模型在推理上花了多少Token - 如果你处理多模态输入,
image和prompt的比值可以帮你优化图像压缩策略
版本更新与安装
这次更新涉及多个包,建议一起升级:
npm install @node-llm/core@1.17.0 \
@node-llm/mcp@0.2.0 \
@node-llm/orm@0.8.0 \
@node-llm/testing@0.5.1
npm install @node-llm/monitor@0.4.2 \
@node-llm/monitor-otel@0.1.1
另外,@node-llm/testing 0.5.1修复了一个罕见的异步竞态问题(VCR磁带自动命名时的),并且把vitest从硬依赖改成了peer依赖——这样你可以用自己的vitest版本,不会出现版本冲突。
总结
NodeLLM 1.17是一次很扎实的更新:
- MCP采样完成了MCP协议的闭环,让服务器可以安全地委托LLM调用给客户端
- 并发工具执行解决了长期存在的性能问题,让独立工具调用可以并行执行
- 回调叠加修复了一个隐蔽的bug,让多个回调可以安全共存
- ORM工具控制让持久化层和核心层的工具行为保持一致
- Monitor Token分类让Token使用分析更加精细和准确
每个改动都有明确的使用场景和设计考量,不是单纯为了加功能而加功能。如果你在用NodeLLM做生产级应用,这次更新值得认真看看。
完整的变更列表可以查看Commit History和CHANGELOG。有问题欢迎在评论区讨论。