普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月17日技术

5大核心分析维度+3种可视化方案:脑肿瘤大数据分析系统全解析 毕业设计 选题推荐 毕设选题 数据分析 机器学习

2026年1月17日 15:51
涵盖患者画像、肿瘤特征、治疗方案、症状关联及风险因素五大模块,通过Python与Django后端处理数据,Vue与Echarts前端展示,为医疗数据分析提供直观工具。

2026 年 Node.js + TS 开发:别再纠结 nodemon 了,聊聊热编译的最优解

作者 donecoding
2026年1月17日 14:18

在开发 Node.js 服务端时,“修改代码 -> 自动生效”的开发体验(即热编译/热更新)是影响效率的关键。随着 Node.js 23+  原生支持 TS 以及 Vite 5 的普及,我们的工具链已经发生了巨大的更迭。

今天我们深度拆解三种主流的 Node.js TS 开发实现方式,帮你选出最适合 2026 年架构的方案。


一、 方案对比大盘点

方案 核心原理 优点 缺点 适用场景
tsx (Watch Mode) 基于 esbuild 的极速重启 零配置、性能强、生态位替代 nodemon 每次修改重启整个进程,状态丢失 小型服务、工具脚本
vite-node 基于 Vite 的模块加载器 完美继承 Vite 配置、支持模块级 HMR 配置相对复杂,需手动处理 HMR 逻辑 中大型 Vite 全栈项目
Node.js 原生 Node 23+ Type Stripping 无需第三方依赖,官方标准 需高版本 Node,功能相对单一 追求极简、前瞻性实验

二、 方案详解

  1. 现代替代者:tsx —— 告别 nodemon + ts-node

过去我们常用 nodemon --exec ts-node,但在 ESM 时代,这套组合经常报 ERR_UNKNOWN_FILE_EXTENSION 错误。

tsx 内部集成了 esbuild,它是目前 Node 18+ 环境下最稳健的方案。

  • 实现热编译:

    bash

    npx tsx --watch src/index.ts
    

    请谨慎使用此类代码。

  • 为什么选它:  它不需要额外的加载器配置(--loader),且 watch 模式非常智能,重启速度在毫秒级。

  1. 开发者体验天花板:vite-node —— 真正的 HMR

如果你已经在项目中使用 Vite 5,那么 vite-node 是不二之选。它不仅是“重启”,而是“热替换”。

  • 核心优势:

    • 共享配置:直接复用 vite.config.ts 中的 alias 和插件。
    • 按需编译:只编译当前运行到的模块,项目越大优势越明显。
  • 实现热更新(不重启进程):

    typescript

    // src/index.ts
    import { app } from './app';
    let server = app.listen(3000);
    
    if (import.meta.hot) {
      import.meta.hot.accept('./app', (newModule) => {
        server.close(); // 优雅关闭旧服务
        server = newModule.app.listen(3000); // 启动新逻辑,DB连接可复用
      });
    }
    

    请谨慎使用此类代码。

  1. 官方正统:Node.js 原生支持

如果你能使用 Node.js 23.6+ ,那么可以摆脱所有构建工具。

  • 运行:  node --watch src/index.ts
  • 点评:  这是未来的趋势,但在 2026 年,由于生产环境往往还停留在 Node 18/20 LTS,该方案目前更多用于本地轻量级开发。

三、 避坑指南:Vite 5 打包 Node 服务的报错

在实现热编译的过程中,如果你尝试用 Vite 打包 Node 服务,可能会遇到:

Invalid value for option "preserveEntrySignatures" - setting this option to false is not supported for "output.preserveModules"

原因:  当你开启 preserveModules: true 想保持源码目录结构输出时,Rollup 无法在“强制保留模块”的同时又“摇树优化(Tree Shaking)”掉入口导出。

修复方案:
在 vite.config.ts 中明确设置:

typescript

build: {
  rollupOptions: {
    preserveEntrySignatures: 'exports-only', // 显式声明保留导出
    output: {
      preserveModules: true
    }
  }
}

请谨慎使用此类代码。


四、 总结:我该选哪个?

  1. 如果你只想快速写个接口,不想折腾配置:请直接使用 tsx。它是 2026 年 nodemon 的完美继承者。
  2. 如果你在做复杂全栈项目,或者有大量的路径别名:请使用 vite-node。它能让你在 Node 端获得跟前端 React/Vue 编写时一样丝滑的 HMR 体验。
  3. 如果是为了部署生产环境:无论开发环境用什么,生产环境请务必通过 vite build 产出纯净的 JS,并使用 node dist/index.js 运行。

使用 LangChain.js 在node端 连接glm大模型示例

作者 kevinIcoding
2026年1月17日 12:47

使用 LangChain 在后端连接大模型:实践指南 🚀

本文以实战项目代码为例,包含完整后端接入、流式(SSE)实现、前端接收示例与调试方法,读者可直接复制运行。


介绍 ✨

随着大模型在各类应用中的普及,后端如何稳健地接入并把模型能力以 API/流式方式对外提供,成为常见需求。本文基于 LangChain(JS)演示如何在 Node.js 后端(Koa)中:

  • 初始化并调用大模型(示例使用智谱 GLM 的接入方式)
  • 支持普通请求与流式(Server-Sent Events,SSE)响应
  • 在前端用 fetch 读取流并实现打字机效果

适用人群:熟悉 JS/TS、Node.js、前端基本知识,想把模型能力放到后端并对外提供 API 的工程师。


一、准备与依赖 🧩

环境:Node.js 16+。

安装依赖(Koa 示例):

npm install koa koa-router koa-bodyparser @koa/cors dotenv
npm install @langchain/openai @langchain/core

在项目根创建 .env

ZHIPU_API_KEY=你的_api_key_here
PORT=3001

提示:不同模型提供方的 baseURL 与认证字段会不同,请根据提供方文档调整。


二、后端:服务封装(chatService)🔧

把模型调用封装到服务层,提供普通调用与流式调用接口:

// backend/src/services/chatService.js
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, SystemMessage } from "@langchain/core/messages";

const chatService = {
  llm: null,

  init() {
    if (!this.llm) {
      this.llm = new ChatOpenAI({
        openAIApiKey: process.env.ZHIPU_API_KEY,
        modelName: "glm-4.5-flash",
        temperature: 0.7,
        configuration: { baseURL: "https://open.bigmodel.cn/api/paas/v4/" },
      });
    }
  },

  async sendMessage(message, conversationHistory = []) {
    this.init();
    const messages = [
      new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
      ...conversationHistory.map((msg) =>
        msg.role === "user"
          ? new HumanMessage(msg.content)
          : new SystemMessage(msg.content),
      ),
      new HumanMessage(message),
    ];

    try {
      const response = await this.llm.invoke(messages);
      return {
        success: true,
        content: response.content,
        usage: response.usage_metadata,
      };
    } catch (error) {
      console.error("聊天服务错误:", error);
      return { success: false, error: error.message };
    }
  },

  async sendMessageStream(message, conversationHistory = []) {
    this.init();
    const messages = [
      new SystemMessage("你是一个有帮助的 AI 助手,使用中文回答问题。"),
      ...conversationHistory.map((msg) =>
        msg.role === "user"
          ? new HumanMessage(msg.content)
          : new SystemMessage(msg.content),
      ),
      new HumanMessage(message),
    ];

    try {
      // 假设 llm.stream 返回异步迭代器,逐 chunk 返回 { content }
      const stream = await this.llm.stream(messages);
      return { success: true, stream };
    } catch (error) {
      console.error("流式聊天服务错误:", error);
      return { success: false, error: error.message };
    }
  },
};

export default chatService;

说明:实际 SDK 接口名(如 invokestream)请依据你所用的 LangChain / provider 版本调整。


三、控制器:普通与 SSE 流式(Koa)🌊

SSE 要点:需要直接写原生 res,并设置 ctx.respond = false,防止 Koa 在中间件链结束时覆盖响应或返回 404。

// backend/src/controllers/chatController.js
import chatService from "../services/chatService.js";

const chatController = {
  async sendMessage(ctx) {
    try {
      const { message, conversationHistory = [] } = ctx.request.body;
      if (!message) {
        ctx.status = 400;
        ctx.body = { success: false, error: "消息内容不能为空" };
        return;
      }
      const result = await chatService.sendMessage(
        message,
        conversationHistory,
      );
      if (result.success) ctx.body = result;
      else {
        ctx.status = 500;
        ctx.body = result;
      }
    } catch (error) {
      ctx.status = 500;
      ctx.body = { success: false, error: "服务器内部错误" };
    }
  },

  async sendMessageStream(ctx) {
    try {
      const { message, conversationHistory = [] } = ctx.request.body;
      if (!message) {
        ctx.status = 400;
        ctx.body = { success: false, error: "消息内容不能为空" };
        return;
      }

      const result = await chatService.sendMessageStream(
        message,
        conversationHistory,
      );
      if (!result.success) {
        ctx.status = 500;
        ctx.body = result;
        return;
      }

      ctx.set("Content-Type", "text/event-stream");
      ctx.set("Cache-Control", "no-cache");
      ctx.set("Connection", "keep-alive");
      ctx.status = 200;
      // 关键:让我们直接操作 Node 原生 res
      ctx.respond = false;

      for await (const chunk of result.stream) {
        const content = chunk.content;
        if (content) ctx.res.write(`data: ${JSON.stringify({ content })}\n\n`);
      }

      ctx.res.write("data: [DONE]\n\n");
      ctx.res.end();
    } catch (error) {
      console.error("流式控制器错误:", error);
      ctx.status = 500;
      ctx.body = { success: false, error: "服务器内部错误" };
    }
  },
};

export default chatController;

四、路由与启动 🌐

// backend/src/routes/index.js
import Router from "koa-router";
import chatController from "../controllers/chatController.js";
const router = new Router({ prefix: "/api" });
router.post("/chat", chatController.sendMessage);
router.post("/chat/stream", chatController.sendMessageStream);
export default router;

// backend/src/app.js
import dotenv from "dotenv";
import Koa from "koa";
import bodyParser from "koa-bodyparser";
import cors from "@koa/cors";
import router from "./routes/index.js";

dotenv.config();
const app = new Koa();
app.use(cors({ origin: "*", credentials: true }));
app.use(bodyParser());
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(process.env.PORT || 3001, () => console.log("Server running"));

五、前端:接收 SSE 流并实现“打字机”效果 ⌨️

前端用 fetch + ReadableStream 读取 SSE 后端发送的 chunk(格式为 data: {...}\n\n)。下面给出简洁示例:

// frontend/src/services/chatService.ts (核心片段)
export const sendMessageStream = async (
  message,
  conversationHistory,
  onChunk,
  onComplete,
  onError,
) => {
  try {
    const response = await fetch("/api/chat/stream", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message, conversationHistory }),
    });

    if (!response.ok) throw new Error("网络请求失败");
    const reader = response.body.getReader();
    const decoder = new TextDecoder();

    let buffer = "";
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = decoder.decode(value);
      // 简单按行解析 SSE data: 行
      const lines = chunk.split("\n");
      for (const line of lines) {
        if (line.startsWith("data: ")) {
          const data = line.slice(6);
          if (data === "[DONE]") {
            onComplete();
            return;
          }
          try {
            const parsed = JSON.parse(data);
            if (parsed.content) onChunk(parsed.content);
          } catch (e) {}
        }
      }
    }
  } catch (err) {
    onError(err.message || "流式请求失败");
  }
};

打字机效果思路(前端)📌:

  • 后端 chunk 通常是按小段返回,前端把每个 chunk 追加到 buffer
  • 用一个定时器以固定速度(如 20–40ms/字符)把 buffer 的字符逐个移动到展示内容,使文本逐字出现。
  • onComplete 时快速显示剩余字符并停止定时器。

你可以参考项目中 App.tsx 的实现(已实现逐 chunk 追加与打字机渲染逻辑)。


App.tsx

import React, { useState } from "react";
import MessageList from "./components/MessageList";
import ChatInput from "./components/ChatInput";
import { chatService, Message } from "./services/chatService";
import "./App.css";

const App: React.FC = () => {
  const [messages, setMessages] = useState<Message[]>([]);
  const [isStreaming, setIsStreaming] = useState(false);

  const handleSendMessage = async (message: string) => {
    const userMessage: Message = { role: "user", content: message };
    setMessages((prev) => [...prev, userMessage]);
    setIsStreaming(true);

    let assistantContent = "";
    const conversationHistory = messages;

    await chatService.sendMessageStream(
      message,
      conversationHistory,
      (chunk: string) => {
        assistantContent += chunk;
        setMessages((prev) => {
          const newMessages = [...prev];
          const lastMessage = newMessages[newMessages.length - 1];
          if (lastMessage && lastMessage.role === "assistant") {
            lastMessage.content = assistantContent;
          } else {
            newMessages.push({ role: "assistant", content: assistantContent });
          }
          return newMessages;
        });
      },
      () => {
        setIsStreaming(false);
      },
      (error: string) => {
        console.error("流式响应错误:", error);
        setMessages((prev) => [
          ...prev,
          { role: "assistant", content: "抱歉,发生了错误,请稍后重试。" },
        ]);
        setIsStreaming(false);
      },
    );
  };

  return (
    <div className="app">
      <header className="app-header">
        <h1>🤖 LangChain + 智谱 GLM</h1>
        <p>AI 聊天助手</p>
      </header>
      <main className="app-main">
        <MessageList messages={messages} isStreaming={isStreaming} />
        <ChatInput onSendMessage={handleSendMessage} disabled={isStreaming} />
      </main>
    </div>
  );
};

export default App;

MessageList

import React, { useRef, useEffect } from "react";
import { Message } from "../services/chatService";

interface MessageListProps {
  messages: Message[];
  isStreaming: boolean;
}

const MessageList: React.FC<MessageListProps> = ({ messages, isStreaming }) => {
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
  }, [messages, isStreaming]);

  return (
    <div className="message-list">
      {messages.length === 0 ? (
        <div className="empty-state">
          <p>👋 欢迎使用 LangChain + 智谱 GLM 聊天助手!</p>
          <p>开始提问吧,我会尽力帮助你 💬</p>
        </div>
      ) : (
        messages.map((msg, index) => (
          <div key={index} className={`message ${msg.role}`}>
            <div className="message-avatar">
              {msg.role === "user" ? "👤" : "🤖"}
            </div>
            <div className="message-content">
              <div className="message-role">
                {msg.role === "user" ? "用户" : "AI 助手"}
              </div>
              <div className="message-text">{msg.content}</div>
            </div>
          </div>
        ))
      )}
      {isStreaming && (
        <div className="message assistant">
          <div className="message-avatar">🤖</div>
          <div className="message-content">
            <div className="message-role">AI 助手</div>
            <div className="message-text streaming">
              <span className="typing-indicator">...</span>
            </div>
          </div>
        </div>
      )}
      <div ref={messagesEndRef} />
    </div>
  );
};

export default MessageList;

六、调试建议与常见坑 ⚠️

  • 404:确认前端请求路径 /api/chat/stream 与路由前缀一致;开发时若使用 Vite,请在 vite.config.ts 配置 proxy 到后端端口。
  • SSE 返回 404/空响应:确认控制器里 ctx.respond = false 已设置,并且在设置 header 后立即开始 ctx.res.write
  • headers already sent:不要在写入原生 res 后再次设置 ctx.bodyctx.status
  • CORS:若跨域,确保后端 CORS 配置允许 Content-TypeAuthorization 等必要 header。

七、快速上手测试命令 🧪

启动后端:

# 在 backend 目录下
node src/app.js
# 或使用 nodemon
npx nodemon src/app.js

用 curl 测试流式(查看快速返回流):

curl -N -H "Content-Type: application/json" -X POST http://localhost:3001/api/chat/stream -d '{"message":"你好","conversationHistory":[]} '

你应能看到类似:

data: {"content":"你"}
data: {"content":"好"}
data: [DONE]

推荐一个超好用的全栈通用瀑布流布局库 universal-waterfall-layout

作者 Summer不秃
2026年1月17日 12:00

在前端开发中,瀑布流布局(Masonry Layout)一直是一个让人又爱又恨的需求。爱它是因为它能极大提升图片类应用(如 Pinterest、小红书)的视觉体验和空间利用率;恨它是因为实现起来坑点满满——图片高度不固定、窗口缩放重排、框架兼容性问题等等。

今天给大家推荐一个最近发现的宝藏开源库:universal-waterfall-layout

正如其名,不仅仅是给 Vue 用,也不仅仅是给 React 用,它是一个**通用(Universal)**的解决方案。

为什么选择它?

现在的市面上的瀑布流插件往往绑定特定框架,或者包体积过大。而 universal-waterfall-layout 有几个非常击中痛点的特性:

1. 🚀 全栈通用,一次学习

无论你是 Vue 3 爱好者,还是 React 拥趸,甚至是 原生 JS 的死忠粉,这个库都能无缝支持。它的核心逻辑是用原生 TypeScript 编写的,零依赖,仅仅在核心之上封装了轻量级的 Vue 和 React 组件适配层。

2. ⚡️ 内置"刚需"功能

很多瀑布流插件只管布局,不管体验。但这个库在 v1.0.3 版本中贴心地加入了很多实战刚需功能:

  • 骨架屏 (Skeleton Loading): 数据还没回来?直接通过 props 开启加载状态,自带脉冲动画的骨架屏,拒绝白屏。
  • 空状态 (Empty State): 列表为空时自动显示占位提示,不再需要自己写 v-if/v-else
  • 图片懒加载 (Lazy Load): 内置原生图片懒加载支持,性能直接拉满,不需要再引入额外的 lazyload 库。

3. 📐 两种核心布局策略

它不仅仅是简单的“排列”,还提供了两种最常用的响应式策略:

  • 固定列宽 (Fixed Column Width): 指定每列 250px,容器会自动计算能放下几列,并自动居中。适合大屏展示。
  • 固定列数 (Fixed Column Count): 指定“我要 3 列”,无论屏幕多大,宽度都会自动拉伸填满。适合移动端 H5。

快速上手

安装

非常简单,体积也很小:

npm install universal-waterfall-layout
# 或者
pnpm add universal-waterfall-layout

via Vue 3

在 Vue 中使用非常丝滑,你可以直接利用 slot 来自定义加载中和空状态的 UI:

<template>
  <Waterfall 
    :gap="20" 
    :columnWidth="250"
    :loading="isLoading"
    :lazyload="true"
  >
    <!-- 数据列表 -->
    <div v-for="item in items" :key="item.id" class="card">
      <img :src="item.image" alt="" />
      <p>{{ item.title }}</p>
    </div>
    
    <!-- 自定义加载骨架 -->
    <template #loading>
       <div class="my-skeleton">正在加载精彩内容...</div>
    </template>

    <!-- 自定义空状态 -->
    <template #empty>
       <div class="empty-box">暂无相关数据</div>
    </template>
  </Waterfall>
</template>

<script setup>
import { Waterfall } from 'universal-waterfall-layout/vue';
import { ref } from 'vue';

const isLoading = ref(true);
const items = ref([]);

// 模拟请求
setTimeout(() => {
  items.value = [{/*...*/}, {/*...*/}];
  isLoading.value = false;
}, 1000);
</script>

via React

React 版本同样拥有优秀的类型提示(TypeScript Friendly):

import { Waterfall } from 'universal-waterfall-layout/react';

const MyGallery = ({ data, loading }) => {
  return (
    <Waterfall 
      gap={15} 
      columnCount={3} // 移动端常用固定3列
      loading={loading}
      lazyload={true}
      loadingComponent={<div>Loading Skeleton...</div>}
      emptyComponent={<div>Nothing here yet!</div>}
    >
      {data.map(item => (
        <div key={item.id} className="card">
           <img src={item.url} />
        </div>
      ))}
    </Waterfall>
  );
};

via Vanilla JS (原生 HTML)

如果你不使用任何框架,或者在传统的 HTML 页面中使用,也完全没问题。核心库提供了直接操作 DOM 的能力:

<div id="waterfall-container">
  <div class="item"><img src="..." /></div>
  <div class="item"><img src="..." /></div>
  <!-- ... -->
</div>

<script type="module">
  import { WaterFallCore } from 'universal-waterfall-layout';

  const container = document.getElementById('waterfall-container');
  
  const waterfall = new WaterFallCore({
    container: container, // 必填:容器 DOM 元素
    gap: 15,              // 间距
    columnWidth: 220,     // 固定列宽策略
    lazyload: true        // 开启懒加载
  });
  
  // 如果后续通过 JS 动态添加了元素,可以手动触发布局更新
  // waterfall.updateItems();
  // waterfall.layout();
</script>

结语

在造轮子泛滥的今天,找到一个克制且好用的库并不容易。universal-waterfall-layout 没有过度封装,但把最核心的布局算法响应式处理加载体验做得非常扎实。

如果你正在开发图片流、商品墙或者作品集展示页面,强烈推荐试一试!

🔗 NPM 地址: universal-waterfall-layout

通过英伟达平台免费调用 GLM4.7 教程

作者 子洋
2026年1月17日 11:39

前言

最近在折腾 AI Agent 和模型接入相关的事情时,意外发现英伟达居然提供了一个面向开发者、可以免费调用模型 API 的平台。更关键的是,这个平台上不仅能用到一些主流开源模型,还能直接使用最近热度很高,号称开源 Coding 能力最强的 GLM4.7,以及综合表现相当稳的 minimax-m2.1

说实话,在如今 API 几乎全面 token 计费、随便一个复杂任务就轻松几十万甚至上百万 token 的背景下,这种可以正经做开发实验、不用一上来就烧钱的平台,对个人开发者和学习阶段的人来说非常友好。所以这篇文章主要做三件事:

  • 介绍 NVIDIA Build 平台本身能做什么
  • 记录从注册到实际调用 API 的完整流程
  • 分享一段真实使用下来的模型体验与限制

整体偏实践,结论也会尽量基于实际使用情况展开。

NVIDIA Build 模型平台

NVIDIA Build可以理解为英伟达官方提供的一个模型集成与调试平台。平台上已经部署了大量模型,涵盖文生文(Chat / Reasoning / Coding)、文生图 / 图生文、语音相关等模型。目前平台上可见的模型数量在 200+,基本覆盖了市面上主流的开源模型生态,例如:deepseek-R1 / deepseek-v3.xqwen3 / qwen-coderkimi-k2minimax-m2.1z-ai/glm4.7,平台本身还提供了在线 Playground(支持参数调节、tools 调用)、OpenAI 风格的 API 接口、模型示例代码一键生成等能力。

注册账号与 API Key 申请

账号注册说明

注意:疑似因为近期注册用户激增,新账号存在一定概率无法正常申请 API Key 的问题。在不影响账号合规性的前提下,比较稳妥的做法是使用非国内常见的邮箱注册,例如相对少见的邮箱(yeah.net),或国外邮箱(gmail.com)等,以及注册时使用浏览器无痕窗口,避免历史状态干扰。

创建账号

访问:build.nvidia.com/ ,点击左上角 Login

在弹窗中输入邮箱并点击 Next,随后填写注册信息并完成人机验证。

这里需要注意:在“更多注册选项”中可以看到 QQ、微信等方式,但不建议使用第三方快捷登录。在当前阶段,使用这些方式注册后,账号更容易出现 API 权限受限的情况。

完成注册后,会进入一些偏个性化设置的步骤(例如名称、偏好选项),按需填写即可。

如果账号状态正常,稍等片刻后,页面顶部会出现提示:

Please verify your account to get API access

点击右上角的 Verify 进入验证流程。

手机号验证

在验证弹窗中,将默认的 +1 修改为 +86,输入国内手机号即可。这里不需要刻意规避,国内手机号是可以正常通过验证的

点击 Send Code via SMS,完成验证码验证。

创建 API Key

验证完成后,点击右上角头像,进入 API Keys 管理页面。

如果账号状态正常,这里可以看到 Generate API Key 按钮。

点击后,输入一个 Key 名称(仅用于区分),过期时间选择 Never Expire

生成完成后,复制并妥善保存该 API Key,后续调用只会展示一次。

如果在 API Keys 页面完全看不到生成按钮,而是类似下图所示的提示界面,基本可以确认该账号当前无法使用 API 功能,建议更换账号重新注册。

使用 API Key 调用

本地客户端配置

只要是支持 OpenAI 风格接口的客户端基本都可以直接使用,我这里以 Jan 为例。

进入设置页,添加一个新的模型提供商。

  • Provider Name:自定义(例如 Nvidia
  • API Key:填写刚刚生成的 Key
  • Base URL:https://integrate.api.nvidia.com/v1

完成后添加模型。

例如添加 GLM4.7:

z-ai/glm4.7

新建会话并选择该模型后,即可正常对话。从体感上看,在普通对话场景下 token 输出速度非常快

获取模型列表

Jan 也支持直接调用 /models 接口获取模型列表,点击刷新即可自动拉取并添加。

需要注意的是:

  • /models 返回的是平台全量模型列表
  • 其中包含文生图、语音、多模态等模型
  • 并非所有模型都支持 chat / text-to-text

因此,如果在客户端中直接选择不支持 chat 的模型发送消息,会直接报错,这是模型能力不匹配,不是接口问题。

Playground 与模型调试

在 NVIDIA Build 平台的 Models 页面中,可以通过搜索 Chat / Reasoning 筛选支持的模型,或者在 Playground 页面的左上角看到所有支持文生文的模型列表。

kimi-k2 为例,点击模型后可以进入在线调试界面。

  • 左侧 Tools:可启用模型支持的工具
  • 右侧 Parameters:控制温度、最大 token 等参数

点击右上角 View Code,可以直接看到对应的 API 调用示例,包括 Base URL、Model ID、Messages 结构等。

Tools 调用示例

在部分模型中可以直接启用 tools,这里以 minimax-m2 为例演示。

启用 get_current_weather 工具后,询问某地天气,模型会自动进行 tools 规划与调用,并返回结果。

再次点击 View Code,可以看到完整的 tools 调用示例代码。

模型与接口

NVIDIA Build 提供的是 OpenAI 风格 API,接口层面兼容 chat.completions / responses,是否支持 chattools、多模态,取决于模型本身。所以,最稳妥的方式仍然是在平台 Models 页面中筛选 chat / reasoning,再决定是否接入到本地客户端或代码中。

使用体验与限制

说一下 GLM4.7 这个模型。它并不是我第一次用,在刚发布不久时我就已经通过一些第三方 API 供应商接触过,这次算是第二次较完整地使用。综合两次实际开发体验,说实话体感并不算好。

首先一个比较明显的问题是,在我目前常用的模型里(比如 qwen-code、gpt-5.1、gpt-5.2、Claude 等),只有 GLM4.7 会在生成的文件头部插入 Markdown 的代码块标记。这个问题在代码编辑和文件生成场景下尤其影响体验,需要额外清理,看起来就很蠢。

其次是执行效率问题,这一点让我感觉很奇怪。纯对话场景下它的响应速度是很快的,但一旦进入干活模式,比如稍微复杂一点的任务编排、代码修改或多步执行,单个任务可能会跑十几甚至二十分钟。问题不在于我不能接受模型执行复杂任务耗时,而是过程中偶尔会出现明显的停顿或卡住一段时间再继续,节奏非常不稳定。

一开始我也怀疑是 API 调用频率或限流导致的,但后来在同样的客户端、同样的任务复杂度下切换到 minimax-m2,发现并不是这个原因。minimax 的整体执行节奏要顺畅得多,调用也更激进,甚至可以轻松跑到 40 次 / 分钟 的平台上限,当然代价就是一旦规划稍微激进,就很容易直接撞上限流,接口报错,任务中断。

从平台层面来看,这个平台整体体验其实是非常不错的:模型选择多、接入成本低、示例清晰,对学习和实验阶段的开发者非常友好。平台的限制也比较直观明确,比如 API 调用频率限制在 40 次 / 分钟,超出后直接返回错误,这一点在 minimax-m2 上体现得尤为明显。

回到 GLM4.7 本身,客观来说它的功能是完全正常的:工具调用没问题,代码编辑能用,对话速度也快,只是在复杂任务执行阶段明显偏慢,且稳定性不够好。相比之下,minimax-m2 在相同条件下执行节奏更线性、更听话,只是更容易触发平台限流 (当然了,因为频繁触发限流所以我也没深度使用 minimax)

总结来说,GLM4.7 并不是不能干活,但实际开发体验一般,尤其是在需要长时间、连续执行任务的场景下,效率和节奏上的问题会被放大。

结语

实话说,这个平台在当前这个时间点,真的算是相当良心的存在。对想学习 AI Agent、工具调用、多模型编排的开发者来说,能够在不额外付费的情况下反复试错,本身就很有价值。

当然了,平台策略和风控状态可能随时变化,如果只是想白嫖一点体验,建议还是尽早注册,至少在账号状态正常的前提下,把 API Key 拿到手。

至于模型怎么选,建议多试、多对比,别迷信单一模型。能稳定把活干完的模型,才是好模型。

相关链接

【节点】[Integer节点]原理解析与实际应用

作者 SmalBox
2026年1月17日 10:56

【Unity Shader Graph 使用与特效实现】专栏-直达

在Unity URP Shader Graph中,Integer节点是一个基础但功能强大的工具节点,它允许开发者在着色器程序中定义和使用整型常量。虽然着色器编程通常以浮点数运算为主,但整数在特定场景下具有不可替代的作用,特别是在控制流程、数组索引、循环计数和条件判断等方面。

Integer节点的基本概念

Integer节点在Shader Graph中代表一个整型常量值。与其他节点不同,Integer节点不接收输入,而是直接输出一个用户定义的整数值。这个特性使得它成为着色器中的固定参数或控制变量的理想选择。

Integer节点的核心特点

  • 输出值为整型,但在着色器运算中会自动转换为浮点数
  • 可用于控制着色器中的离散状态和条件分支
  • 适合用于数组索引、循环计数和枚举类型的表示
  • 在性能优化方面,整数运算通常比浮点数运算更高效

在Shader Graph中的定位

Integer节点属于Shader Graph的Input类别,与其他的常量节点如Float、Vector2、Vector3等并列。它提供了在可视化着色器编程中处理整数数据的能力,弥补了传统节点图主要以浮点数为中心的设计局限。

节点属性和配置

端口配置

Integer节点只有一个输出端口,其配置如下:

  • 名称:Out
  • 方向:输出
  • 类型:Float
  • 绑定:无
  • 描述:输出整数值,但在类型系统中作为Float处理

端口特性的深入理解

虽然端口类型标记为Float,但实际上输出的是整数值。这种设计是因为HLSL和GLSL着色语言中,整数和浮点数在很多时候可以隐式转换,而且Shader Graph的内部数据类型系统主要以浮点数为基础。在实际着色器代码生成时,这个整数值会被正确地处理为整数类型。

控件参数

Integer节点提供了一个简单的控件用于配置其输出值:

  • 名称:无(在节点上直接显示数值)
  • 类型:Integer
  • 选项:无
  • 描述:定义节点输出的整数值

控件使用要点

  • 可以直接在节点上的输入框中输入整数值
  • 支持正负整数,范围通常受着色语言限制但足够大多数应用场景
  • 数值改变会实时更新节点预览和生成的着色器代码

生成的代码分析

根据官方文档,Integer节点生成的代码示例如下:

HLSL

float _Integer = 1;

代码生成机制深入解析

虽然示例代码显示变量被声明为float类型,但在实际的HLSL编译中,当这个值用于整数上下文时(如数组索引、循环计数器),编译器会进行适当的优化和处理。在更复杂的使用场景中,生成的代码可能会有不同的表现形式:

HLSL

// 当Integer节点用于数组索引时
int index = 2;
float value = _MyArray[index];

// 当用于循环控制时
for (int i = 0; i < _IterationCount; i++)
{
    // 循环体
}

变量命名规则

在生成的代码中,Integer节点对应的变量名称会根据节点在Graph中的名称自动生成。如果节点被命名为"TileCount",则生成的变量可能是_TileCount_Integer_TileCount,具体命名规则取决于Shader Graph的版本和配置。

Integer节点的实际应用

基础数值应用

Integer节点最直接的用途是提供整型常量值,用于控制着色器的各种参数:

  • 平铺和偏移控制:指定纹理平铺次数
  • 循环次数设置:控制for循环的迭代次数
  • 数组大小定义:确定固定大小数组的维度
  • 枚举状态表示:用整数代表不同的渲染状态或材质类型

纹理平铺示例

在纹理采样节点中,使用Integer节点控制平铺参数:

Integer节点(值:4) → TilingAndOffset节点 → SampleTexture2D节点

这种配置可以实现纹理的精确平铺控制,比如确保纹理在模型表面重复恰好4次,而不是4.5次或其他非整数值。

条件逻辑控制

Integer节点在着色器条件逻辑中发挥重要作用,特别是在需要离散状态判断的场景:

  • 多重材质切换:使用整数值选择不同的材质属性集
  • LOD级别控制:根据整数距离值切换细节级别
  • 特效强度分级:将连续的特效参数离散化为几个固定级别

状态机实现示例

通过结合Branch节点和Integer节点,可以实现简单的着色器状态机:

Integer节点(状态值) → Branch节点 → 不同的颜色/纹理输出

数组和循环操作

在高级着色器编程中,数组和循环是常见的编程结构,Integer节点在其中扮演关键角色:

  • 数组索引:安全地访问数组元素
  • 循环计数器:控制固定次数的循环迭代
  • 多维数组处理:计算行主序或列主序数组的索引

数组访问模式

For循环节点(使用Integer节点作为最大值) → 数组索引计算 → 数组元素访问

这种模式常见于图像处理效果,如卷积核操作、多光源累积计算等。

与其他节点的协同工作

与数学节点的配合

Integer节点可以与各种数学节点结合,实现更复杂的数值计算:

  • 算术运算:与Add、Subtract、Multiply、Divide节点配合进行整数运算
  • 比较运算:与Equal、NotEqual、Greater Than、Less Than节点结合实现条件判断
  • 插值运算:虽然整数本身不插值,但可以控制插值参数

运算精度注意事项

当Integer节点参与浮点数运算时,会自动提升为浮点类型。在需要保持整数精度的场景,应尽量避免与浮点数进行混合运算,或确保在关键步骤中使用适当的舍入函数。

与控制流节点的集成

Integer节点与Shader Graph的控制流节点紧密结合,实现动态的着色器行为:

  • Branch节点:使用整数值作为条件输入
  • For循环节点:提供循环次数和索引值
  • Switch节点:作为选择器输入,决定执行哪个分支

性能优化提示

在Shader Graph中使用整数控制流通常比使用浮点数更高效,因为整数比较和分支操作在GPU上的开销较小。特别是在移动平台上,这种优化更为明显。

高级应用技巧

动态整数参数

虽然Integer节点本身表示常量,但可以通过多种方式实现动态的整数参数:

  • 脚本驱动:通过C#脚本在运行时修改材质属性
  • 动画控制:使用Unity动画系统或时间节点驱动整数值变化
  • 顶点数据:从顶点颜色或UV通道中提取整数值

脚本集成示例

CSHARP

// C#脚本中设置整数值
material.SetInt("_IntegerParameter", 5);

数组和数据结构模拟

在着色器中模拟复杂数据结构时,Integer节点用于索引和管理:

  • 查找表索引:访问预计算的查找表
  • 状态矩阵:管理多维状态数组
  • 有限状态机:实现复杂的着色器行为切换

多维度索引计算

通过组合多个Integer节点和数学运算,可以计算多维数组的线性索引:

行索引 × 列数 + 列索引 = 线性索引

性能优化策略

合理使用Integer节点可以显著提升着色器性能:

  • 循环展开优化:使用小的整数值作为循环次数,促进编译器自动展开循环
  • 常量传播:整型常量的优化效果通常比浮点数更好
  • 分支预测:整数条件语句的预测效率通常更高

平台特定考虑

不同GPU架构对整数运算的支持程度不同。在编写跨平台着色器时,应了解目标平台的整数运算特性,特别是在移动设备上的性能表现。

实际案例研究

案例一:离散化颜色调色板

创建一个使用Integer节点选择预定义颜色的着色器:

  • 设置Integer节点作为颜色索引
  • 使用Branch节点或数组索引选择对应颜色
  • 应用选中的颜色到材质表面

这种技术常用于低多边形风格游戏或需要特定颜色方案的应用程序。

案例二:多纹理混合系统

实现一个根据整数值混合多个纹理的系统:

  • 使用Integer节点选择基础纹理、细节纹理和遮罩纹理
  • 根据整数值决定混合模式和强度
  • 创建可配置的多材质系统

案例三:程序化几何生成

在曲面细分或几何着色器中使用Integer节点控制细节级别:

  • 根据距离或重要性设置细分因子
  • 使用整数值确保对称和一致的几何分布
  • 优化性能的同时保持视觉质量

故障排除和最佳实践

常见问题解决

整数精度问题

  • 问题:大整数导致精度丢失或意外行为
  • 解决:确保使用的整数值在合理范围内,通常0-255对于大多数应用足够

类型转换错误

  • 问题:整数到浮点的隐式转换导致意外结果
  • 解决:在关键计算中显式处理类型转换,使用Round、Floor或Ceiling节点

性能问题

  • 问题:使用整数节点后着色器变慢
  • 解决:检查是否创建了复杂的依赖关系,简化节点网络

最佳实践建议

  • 命名规范:为Integer节点使用描述性名称,提高可读性
  • 数值范围:限制整数值在必要的最小范围内,避免不必要的内存占用
  • 文档注释:在Shader Graph中使用注释节点说明Integer节点的用途和预期值范围
  • 测试验证:在不同平台和设备上测试整数相关功能,确保一致的行为

【Unity Shader Graph 使用与特效实现】专栏-直达 (欢迎点赞留言探讨,更多人加入进来能更加完善这个探索的过程,🙏)

❌
❌