阅读视图
🌐 《GraphQL in Next.js 初体验》中文笔记
🧩 一、概览:Next.js + GraphQL 是怎么“对话”的?
Next.js 是前后端一体框架,而 GraphQL 是一种 API 查询语言。结合起来后:
🤝 Next.js 提供运行环境,
🧠 GraphQL 提供数据语义接口。
简单理解:
- Next.js = 舞台和播放器(API 路径、页面渲染、Serverless 运行)
- GraphQL = 剧本(Schema定义)、演员台词逻辑(Resolver)、导演策略(Context)
结构大致如下:
Next.js API Route ─┬─► ApolloServer / Yoga Server
│
└─► GraphQL Schema (定义数据结构)
└─► Resolver (具体处理逻辑)
└─► Context (跨请求共享上下文)
🧱 二、Schema:GraphQL 的“数据契约”
Schema 是 定义数据形状的语言模型。
可以把它理解为数据库表结构 + API 文档的结合体。
示例 ⚙️
# ./graphql/schema.graphql
type User {
id: ID!
name: String!
age: Int
}
type Query {
users: [User!]!
user(id: ID!): User
}
type Mutation {
addUser(name: String!, age: Int): User
}
📘 要点笔记:
-
type:定义类型(用户、文章、评论等) -
Query:读取数据的入口 -
Mutation:修改、创建、删除数据的入口
Schema 就像一份菜单,它定义了服务员能做的所有菜,但不做菜。
做菜的是 Resolver 👉
🧠 三、Resolver:GraphQL 的“大脑中枢”
Resolver 是 Schema 的执行器,它负责将查询请求映射到实际数据源。
在 Next.js 中,一般写成 JS/TS 文件与 Schema 匹配。
示例 👇
// ./graphql/resolvers.js
const users = [
{ id: "1", name: "Neo", age: 29 },
{ id: "2", name: "Trinity", age: 27 },
];
export const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(u => u.id === id),
},
Mutation: {
addUser: (_, { name, age }) => {
const newUser = { id: String(users.length + 1), name, age };
users.push(newUser);
return newUser;
},
},
};
📘 要点笔记:
- 每个字段对应一个函数。
- 第一个参数
_通常是父级字段(这里未使用,可省略)。 - 第二个参数
{ id }是客户端传入的变量。 - Resolver 内不管 Schema 长啥样,它只需返回对应数据。
🌍 四、Context:连接“每个请求”的神经系统
Context 是 GraphQL 在请求周期中共享的环境对象。
它的作用类似于:
- 注入依赖(数据库实例、token 验证、用户状态)
- 跨 Resolver 共享状态
示例:
import jwt from "jsonwebtoken";
import db from "./db.js";
export const createContext = ({ req }) => {
const token = req.headers.authorization || "";
let user = null;
try {
user = jwt.verify(token, process.env.JWT_SECRET);
} catch (e) {
console.log("token 无效或未提供");
}
return { db, user };
};
在 Resolver 中,就能这样访问:
Mutation: {
addUser: (_, { name, age }, { db, user }) => {
if (!user) throw new Error("未授权");
return db.insertUser({ name, age });
}
}
📘 要点笔记:
- Context 在每次请求开始时创建(一次请求一次 Context)。
- 经 Context 可安全访问外部资源且隔离状态。
- 在 SSR 和 Serverless 模式的 Next.js 中非常实用。
⚙️ 五、在 Next.js 中整合 Apollo Server
在 Next.js 中最常见的方式是用 /api/graphql 作为后端入口。
// ./pages/api/graphql.js
import { ApolloServer } from "apollo-server-micro";
import { typeDefs } from "../../graphql/schema.js";
import { resolvers } from "../../graphql/resolvers.js";
import { createContext } from "../../graphql/context.js";
const server = new ApolloServer({
typeDefs,
resolvers,
context: createContext,
});
export const config = {
api: { bodyParser: false },
};
export default server.createHandler({ path: "/api/graphql" });
💡 运行逻辑:
- 浏览器访问
/api/graphql - Next.js 调用 ApolloServer 处理请求
- ApolloServer 根据 Schema 调用对应 Resolver
- Resolver 通过 Context 访问数据源
- 返回 JSON 响应
🖥️ 六、Next.js 前端调用示例
import { gql, useQuery } from "@apollo/client";
const ALL_USERS = gql`
query {
users {
id
name
age
}
}
`;
export default function UsersList() {
const { loading, error, data } = useQuery(ALL_USERS);
if (loading) return <p>⏳ 加载中...</p>;
if (error) return <p>❌ 出错啦: {error.message}</p>;
return (
<ul>
{data.users.map(u => (
<li key={u.id}>
👤 {u.name}(年龄:{u.age ?? "未知"})
</li>
))}
</ul>
);
}
在这里,前端只需声明「我想要什么」,
后端就帮你搞定「怎么算出来」。
💬 七、小结:三大组件一图流
<div style="max-width:720px;margin:auto;text-align:center;">
<svg width="100%" height="300" viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg">
<rect x="60" y="60" width="170" height="60" rx="10" fill="#A1C4FD" stroke="#333"/>
<text x="145" y="95" text-anchor="middle" font-size="13">Schema (定义契约)</text>
<rect x="280" y="60" width="170" height="60" rx="10" fill="#FDD692" stroke="#333"/>
<text x="365" y="95" text-anchor="middle" font-size="13">Resolver (处理逻辑)</text>
<rect x="500" y="60" width="170" height="60" rx="10" fill="#C2E9FB" stroke="#333"/>
<text x="585" y="95" text-anchor="middle" font-size="13">Context (执行环境)</text>
<line x1="230" y1="90" x2="280" y2="90" stroke="#000" stroke-width="2" marker-end="url(#arrow)"/>
<line x1="450" y1="90" x2="500" y2="90" stroke="#000" stroke-width="2" marker-end="url(#arrow)"/>
<defs>
<marker id="arrow" markerWidth="10" markerHeight="10" refX="6" refY="3" orient="auto">
<path d="M0,0 L0,6 L9,3 z" fill="#333"/>
</marker>
</defs>
</svg>
<p style="font-size:13px;color:#666;">▲ Next.js GraphQL 三件套结构关系图</p>
</div>
🚀 八、为什么 Next.js 是 GraphQL 的理想宿主?
- Serverless Ready:API 路由天然适合部署 Apollo 或 Yoga Server。
- SSR + CSR 混合渲染:可直连 GraphQL 数据,从后端直出页面。
- Edge Runtime:未来的 Web AGI 场景(边缘智能体)可直接调用 GraphQL 层。
- TypeScript 一体化支持:Schema + Resolver 均能生成类型定义,开发丝滑。
Node.js + Python 爬虫界的黄金搭档
写了一个工具叫去水印下载鸭,它的功能是解析某音、某红书等平台视频、图片,支持无水印下载。
在后端开发中,我选择了自己最为熟悉的 Node.js 技术。然而,在开发过程中,我遇到了一些棘手的问题,其中最令人头疼的就是某音的加密处理。 幸运的是,我在 GitHub 社区找到了一个开源仓库,它提供了相关的解决方案,但这个仓库是用 Python 实现的。我本想借助 AI 将其转换为 Node.js 代码,但转换后的代码运行报错。为了避免进一步的麻烦,我决定直接在 Node.js 项目中引入这个 Python 仓库,并且删减了其中我不需要的文件。
Node.js 集成 Python
在 Node.js 中使用 Python 简单又高效。我们先让 Python 程序输出结果,然后对结果稍作处理,用 /RESULT_START:结果:RESULT_END/ 这样的格式将其包裹起来。
async def fetch_one_video(url):
"""
Fetches a single video by aweme_id.
"""
hybird_crawler = HybridCrawler()
data = await hybird_crawler.hybrid_parsing_single_video(url,False)
# 只输出结果数据,使用特定标记包裹
print(f"RESULT_START:{json.dumps(data)}:RESULT_END")
随后,在 Node.js 端,我们借助 exec 方法来运行一条 Python 命令,具体如下所示:
new Promise((resolve, reject) => {
const src = path.join(__dirname, '../script/start.py');
exec(`python ${src} ${url}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error executing Python script: ${error}`);
reject(null)
return;
}
if (stderr) {
reject(null)
return;
}
// 使用正则表达式捕获 RESULT_START 和 RESULT_END 之间的内容
const regex = /RESULT_START:(.*?):RESULT_END/;
const match = stdout.match(regex);
if (match && match[1]) {
try {
// 解析捕获到的 JSON 数据
const jsonData = JSON.parse(match[1]);
resolve(jsonData);
} catch (parseError) {
reject(null)
}
} else {
reject(null)
}
})
})
当调用 exec 方法时,Node.js 会调用操作系统的相关功能来创建一个新的子进程。这个子进程会独立于父进程运行。
这样,Node.js环境中使用Python就搞定了。
Docker 部署 Node.js+Python
若想在 Docker 容器中运行 Node.js 与 Python,打包时需确保容器内同时具备 Node.js 和 Python 的运行环境。
最初,Dockerfile 仅包含 Node.js 的配置,配置如下所示:
# 构建阶段
FROM node:24-alpine
WORKDIR /app
# 复制 package 文件
COPY package*.json ./
# 复制源代码
COPY . .
RUN npm i -g pnpm && pnpm i
# 暴露端口
EXPOSE 3000
# 启动应用
CMD [ "npm", "start" ]
为了在Docker镜像中引入Python,我在Dockerfile中加入了对Python3的支持,以确保容器内同时具备Node.js和Python的运行环境。以下是Dockerfile的修改内容:
FROM node:24-alpine
RUN apt-get update && \
apt-get install -y --no-install-recommends python3 python3-pip && \
rm -rf /var/lib/apt/lists/*
#省略...
然而,在构建镜像的过程中,我发现下载python3和python3-pip等包的速度非常慢,并且在安装过程中还涉及到编译,这使得整个构建过程变得异常耗时。
经过一番思考,我决定调整策略:先基于Python镜像构建,再在其上添加Node.js环境。于是,我对Dockerfile进行了如下调整:
FROM python:3.11-slim-bookworm
RUN sed -i 's|http://deb.debian.org|https://mirrors.aliyun.com|g' \
/etc/apt/sources.list.d/debian.sources && \
sed -i 's|http://security.debian.org|https://mirrors.aliyun.com|g' \
/etc/apt/sources.list.d/debian.sources
RUN apt-get update && apt-get install -y --no-install-recommends nodejs npm \
&& rm -rf /var/lib/apt/lists/* \
&& npm config set registry https://registry.npmmirror.com/
#省略...
这样完成解决Docker部署问题。
最后
如果你也遇到了需要在项目中同时使用 Node.js 和 Python 的情况,不妨参考本文的操作方法,或许能为你提供一些思路和帮助。
去水印下载鸭仅限于学习,请勿用于其他用途,否则后果自负,且软件没有进行任何店铺售卖,谨防受骗!
Cursor 2.0 支持模型并发,我用国产 RWKV 模型实现了一模一样的效果 🤩🤩🤩
最近 Cursor 发布 2.0 版本,其中一个比较亮点的功能就是它可以同时指挥 8 个 Agent 执行任务,最后选择你觉得最好的那个答案。
而在底层模型层面,来自中国本土团队的 RWKV 项目也带来了更具突破性的成果:RWKV7-G0a3 13.3B ——当前全球最强的开源纯 RNN 大语言模型。
这一版本以 RWKV6-world-v2.1 14B 为基础,继续训练了 2 万亿 tokens(并融合了 35B 来自 DeepSeek v3.1 的高质量语料),在保持完全 RNN 架构、无注意力机制(No Attention)、无微调、无刷榜的前提下,取得了与主流 Transformer 模型相媲美甚至更优的表现。
在多项权威基准测试中(包括 MMLU、MMLU-Pro、GSM8K、MATH500、CEval 等),RWKV7-G0a3 在语言理解、逻辑推理与数学推演等任务上均实现显著提升。其中,MMLU-Pro 测评显示模型在多学科综合知识上的掌握更加扎实;GSM8K 与 MATH500 结果表明,其在中高难度数学与逻辑问题上的推理能力已达到同规模模型的领先水平。与此同时,RWKV7-G0a3 继续保持了 RWKV 系列一贯的高推理效率与低显存占用优势,展现出纯 RNN 架构在大模型时代下的强大潜力。
Uncheatable Eval 使用最新的论文、新闻、代码与小说等实时数据进行评测,通过“压缩率”(即 Compression is Intelligence)指标,衡量模型在真实语料下的语言建模能力与泛化水平。
MMLU 系列用于测评语言模型在多学科知识与认知推理方面的能力,其中 MMLU Pro 为进阶版本,包含更复杂的问题设计与更严苛的评测标准。
欲获取更多详细信息,请访问该模型的 官方公众号文章 阅读。
这意味着:
在以 Transformer (deep learning architecture) 架构主导的大模型时代,RWKV 所代表的“纯 RNN ”路线再度崛起:以更低的计算与显存成本、更自然的时序记忆机制,走出一条与主流 LLM 截然不同的进化路径。
在 RWKV 命名规则中,G0a3 标识了训练数据在版本与质量上的升级(例如:质量层级为 G# > G#a2 > G#,数据规模层级为 G1 > G0),即便参数量相同,G0a3 系列在泛化能力上也具备潜在优势。综合来看,RWKV7-G0a3 13.3B 的发布,不仅刷新了 RNN 模型性能的新高度,也象征着 RWKV 系列在“摆脱 Transformer 架构垄断”路径上迈出了一步。
模型下载
下载 RWKV7-G0a3 13.3B 模型(.pth 格式):
-
Hugging Face: huggingface.co/BlinkDL/rwk…
-
模搭 (ModelScope): modelscope.cn/models/RWKV…
-
Wisemodel: download.wisemodel.cn/file-proxy/…
下载 .gguf 格式: modelscope.cn/models/shou…
下载 Ollama 格式: ollama.com/mollysama
如何使用 RWKV 模型(本地部署)
可以使用 RWKV Runner、Ai00 或 rwkv pip 等推理工具在本地部署 RWKV 模型。
RWKV 模型同时兼容主流推理框架,如 llama.cpp 与 Ollama。
目前最快的 RWKV 推理工具是 Albatross。
由于 RWKV7-G0a3 13.3B 属于新模型,建议优先使用 RWKV Runner 以确保结果稳定与准确。
更多关于部署与推理的使用教程,可参考 RWKV 官网 - 模型部署和推理教程。
前端如何实现模型并发的效果
首先,我们要知道模型并发的效果,那我们要知道连接了发起了一个请求之后,它是怎么回复的:
我们现在对的网络请求已经进行了截取,这是其中的一些数据,我们将一下核心的数据写到 json 文件里面让他能够更好的展示:
首先我们知道这是一个流式返回,但是一次流式返回了包含了的内容非常多,这里就是我们并发的关键了,这里的 index 代表并发的下标,而 delta.content 是具体的内容,这样我们知道了 SSE 实现并发的原理了,实际上就是调用 SSE,后端在一次 SSE 的返回中返回同一个问题的不同的结果并通过下标来区分。
我们已经把 SSE 返回机制摸清楚了。下面就轻松地走一遍“边生成边展示”的整个流程:从流式到达、到何时更新、再到怎么把半成品 HTML 安全地渲染出来,最后配上 UI 的滚动与分批加载。读完你就能一眼看懂这套实时渲染是怎么跑起来的。
先说结论:这件事其实就五步,顺次串起来就好了——流式接收、增量累积与触发、HTML 提取与补全、UI 局部更新、以及 iframe 的分批渲染。下面逐段拆开讲。
一、流式数据接收(ai.ts:195–251)
// 使用 ReadableStream 读取流式数据
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let partial = ""; // 处理不完整的行
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
partial += chunk;
// 逐行解析 SSE 格式:data: {...}
const lines = partial.split("\n");
partial = lines.pop() || ""; // 保留不完整的行
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const json: StreamChunk = JSON.parse(data);
// 处理每个 chunk...
}
}
这里的代码的核心要点如下:
- 逐行处理:SSE 数据按行到达,
split('\n')拆开,半截 JSON 用partial暂存。 - 字符安全:
TextDecoder(..., { stream: true })负责拼接多字节字符,避免中文或 emoji 被截断。 - 过滤噪声:只解析
data:开头的有效行,忽略心跳、空行、注释。 - 流式收尾:遇到
[DONE]仅结束对应流 index,其余继续处理。
二、增量累积与智能触发(ai.ts:224–244)
// 为每个 index 累积内容
contentBuffers[index] += delta;
// 判断是否应该触发渲染
const lastLength = lastRenderedLength.get(index) || 0;
const shouldRender = this.shouldTriggerRender(
contentBuffers[index],
lastLength
);
if (shouldRender && onProgress) {
const htmlCode = this.extractHTMLCode(contentBuffers[index]);
onProgress(index, contentBuffers[index], htmlCode);
lastRenderedLength.set(index, contentBuffers[index].length);
}
这里的代码的核心要点如下:
- 多流并行:每个 index 各自独立累积,互不干扰。
- 智能触发:通过与
lastRenderedLength比较控制频率,避免“来一点就刷”。 - 精准更新:只触发对应 index 的渲染,避免全局重排。
- 兜底刷新:流结束后进行最终更新,确保结果完整。
三、触发策略(ai.ts:62–101)
private static shouldTriggerRender(
newContent: string,
oldLength: number,
): boolean {
// 1. 首次渲染:内容超过 20 字符
if (oldLength === 0 && newLength > 20) {
return true;
}
// 2. 关键闭合标签出现(语义区块完成)
const keyClosingTags = [
'</header>', '</section>', '</main>',
'</article>', '</footer>', '</nav>',
'</aside>', '</div>', '</body>', '</html>'
];
const addedContent = newContent.substring(oldLength);
for (const tag of keyClosingTags) {
if (addedContent.includes(tag)) {
return true; // 区块完成,立即渲染
}
}
// 3. 内容增长超过 200 字符(防止长时间不更新)
if (newLength - oldLength > 200) {
return true;
}
return false;
}
这里的代码的核心要点如下:
- 首帧提速:内容首次超过 20 字符立即渲染,减少“首屏空白”。
- 语义闭合优先:检测新增片段中的关键闭合标签(如
</section>、</div>),保证块级内容完整展示。 - 超长兜底:即使未闭合,增量超 200 字符也强制刷新。
- 性能友好:仅比较“新增部分”,无需重复扫描旧文本;参数可根据模型节奏与设备性能调节。
四、HTML 提取与自动补全(ai.ts:19–59, 104–145)
private static extractHTMLCode(content: string): string {
// 方式1: 完整的 ```html 代码块
const codeBlockMatch = content.match(/```html\s*([\s\S]*?)```/);
if (codeBlockMatch) return codeBlockMatch[1].trim();
// 方式2: 未完成的代码块(流式渲染)
const incompleteMatch = content.match(/```html\s*([\s\S]*?)$/);
if (incompleteMatch) {
return this.autoCompleteHTML(incompleteMatch[1].trim());
}
// 方式3: 直接以 <!DOCTYPE 或 <html 开头
if (trimmed.startsWith('<!DOCTYPE') || trimmed.startsWith('<html')) {
return this.autoCompleteHTML(trimmed);
}
return '';
}
private static autoCompleteHTML(html: string): string {
// 移除最后不完整的标签(如 "<div cla")
if (lastOpenBracket > lastCloseBracket) {
result = html.substring(0, lastOpenBracket);
}
// 自动闭合 script、body、html 标签
// 确保浏览器可以渲染未完成的 HTML
return result;
}
这里的代码的核心要点如下:
- 多格式兼容:完整块、未闭合块、裸 HTML 均可识别。
- 容错补齐:遇到半截标签(如
<div cla)自动裁剪,再补上</script>、</body>、</html>等关键闭合。 - 最小修正:仅做“可渲染”层面的修复,保持生成内容原貌。
- 安全回退:提不出 HTML 时返回空字符串,避免将解释性文字误渲染。
五、UI 实时更新(ChatPage.tsx:240–253)
await AIService.generateMultipleResponses(
userPrompt,
totalCount,
(index, content, htmlCode) => {
// 实时更新对应 index 的结果
setResults((prev) =>
prev.map((result, i) =>
i === index
? {
...result,
content, // 原始 Markdown 内容
htmlCode, // 提取的 HTML 代码
isLoading: false,
}
: result
)
);
}
);
这里的代码的核心要点如下:
- 局部更新:仅更新目标项,
prev.map保证不可变数据结构,减少重渲染。 - 双轨推进:
content用于 Markdown 文本,htmlCode用于预览展示。 - 快速反馈:首批数据到达即撤骨架屏,让用户感知“正在生成”。
- 状态持久:结果存入
sessionStorage,刷新或返回依旧保留上下文。
六、iframe 分批渲染优化(ChatPage.tsx:109–175)
// 找出已准备好但还未渲染的索引
const readyIndexes = results
.filter(({ result }) => !result.isLoading && result.htmlCode)
.map(({ index }) => index)
.sort((a, b) => a - b);
// 第一次渲染:一次性全部加载(用户体验优先)
if (!hasRenderedOnce.current) {
setIframeRenderQueue(new Set(readyIndexes));
hasRenderedOnce.current = true;
return;
}
// 后续渲染:分批加载(每批 8 个,间隔 300ms)
// 避免一次性创建太多 iframe 导致卡顿
const processBatch = () => {
const toAdd = stillNotInQueue.slice(0, batchSize); // 8 个
toAdd.forEach((index) => newQueue.add(index));
if (stillNotInQueue.length > batchSize) {
setTimeout(processBatch, 300); // 继续下一批
}
};
这里的代码的核心要点如下:
- 首批全放:初次渲染不延迟,保证响应速度。
- 后续分批:按批次(默认 8 个/300ms)渐进挂载,防止主线程卡顿。
- 动态调度:每轮重新计算“未入队项”,保证不遗漏。
- 轻量 DOM:仅渲染必要 iframe,滚动与交互更顺滑;参数可按性能灵活调整。
小结
通过语义闭合与字数阈值控制更新频率让画面稳定流畅,HTML 半成品自动补齐避免黑屏,iframe 分批挂载减轻主线程压力并配合 requestAnimationFrame 提升滚动顺滑度,状态由 sessionStorage 兜底并以日志辅助调参;整体逻辑是流式接收边累积、攒到关键点就渲染一帧、UI 精准更新该动的那格,调顺节奏即可实现实时渲染的又快又稳。
效果展示
前面我们说了这么多代码相关的,接下来我们可以把我们的项目运行起来看一下最终运行的效果:
为了让 UI 的效果显示得更好,建议使用 33%缩放的屏幕效果。
在输入框输入我们要问的问题,点击发送,你会看到这样的效果:
这会你能实时看到 24 个页面实时渲染的效果:
这样我们就借助 RWKV7-G0a3 13.3B 模型实现了一个跟 Cursor2.0 版本一模一样的效果了。
总结
RWKV7-G0a3 13.3B 是由中国团队推出的最新一代纯 RNN 大语言模型,在无 Attention 架构下实现了与主流 Transformer 模型相媲美的性能,并在多项基准测试中表现优异。它以更低显存占用和高推理效率展示了 RNN 架构的强大潜力。而前端并发实现中,通过 SSE 流式返回不同 index 的内容,实现了同时生成多个模型响应的并行效果。结合智能触发渲染与分批 iframe 更新,最终达成了类似 Cursor 2.0 的多 Agent 实时对比体验。
Soul 发布超强端侧语音模型,没错,就是你想的那个 Soul 😍😍😍
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777
一、是什么?
SoulX‑Podcast 是由 Soul AI Lab 与 Northwestern Polytechnical University 等联合推出的语音合成模型,专门针对“播客/对话”这种多说话人、多轮、多情境的语音内容而设计。 核心目标包括:
- 支持 多说话人、多轮对话 的语音合成,而不只是传统的一个人朗读。
- 支持 长时段的生成(例如整期播客的长度,而非几句)。
- 支持 多语言/方言(普通话、英语、粤语、四川话、河南话)以及 副语言特征(如笑声、叹气、喘息等)。
- 支持 零样本语音克隆(zero‑shot voice cloning):只用很少目标说话人数据也能生成其风格。
简言之:如果你想自动“生成播客”、“做多人访谈音频”或“创建有地域口音或方言特征”的语音内容,这个模型就是为此类场景量身定制的。
二、主要亮点(为什么很“酷”)
以下是这个模型几个让人印象深刻的地方:
-
对话场景优化:传统 TTS 模型多数优化的是单个说话人说一段话,SoulX‑Podcast 则把“说话人切换”“对话节奏”“多轮往返”放进模型设计里。
-
长时间稳定输出:模型官方测试中能够输出超过 90 分钟的连续对话,并且说话人音色、音质维持稳定。
-
方言 + 跨方言支持:除了标准普通话与英语,还支持四川话、河南话、粤语等方言,并且支持用一种语言的提示生成另一种方言语音(跨方言提示)。
-
副语言控制:你可以在文本里加标签如“<|laughter|>”笑、“<|sigh|>”叹气,让生成语音听起来更“有人味”。模型在这方面识别准确率约 0.82。
-
开源 + 学术支持:代码托管在 GitHub,模型放在 Hugging Face,可用于研究/教育用途。
这里官方提供了一些 Demo,可以去体验一下。
三、能用在哪些场景?
结合其特点,以下是一些很实际的应用场景:
- 自动化播客/访谈:假如你想制作带多个角色、对话式的音频,可以用这个模型生成主持人+嘉宾对话。
- 虚拟主播或角色配音:给虚拟角色配一个有感情、有方言、有特色的声音。
- 方言语音产品:为特定地域群体提供方言语音服务,比如粤语播报、川话讲解。
- 语音克隆与定制化声音:你可以用少量样本,让系统生成你喜欢的声音风格用于朗读、音频书、角色对白等。
- 教育/语言学研究:对话语音、方言语音、非语言符号(如笑、喘)这些过去难以合成的内容,现在变得可用起来了。
四、简单上手指南
如果想快速体验模型,按照以下流程即可:
-
克隆仓库
git clone https://github.com/Soul‑AILab/SoulX‑Podcast.git cd SoulX‑Podcast -
安装环境(推荐用 Conda)
conda create -n soulxpodcast -y python=3.11 conda activate soulxpodcast pip install -r requirements.txt如果在国内,可使用 PyPI 镜像:
pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/ --trusted-host=mirrors.aliyun.com -
下载预训练模型权重
- 基础模型:
SoulX‑Podcast‑1.7B - 方言模型:
SoulX‑Podcast‑1.7B‑dialect
pip install -U huggingface_hub huggingface-cli download --resume-download Soul‑AILab/SoulX‑Podcast‑1.7B --local-dir pretrained_models/SoulX‑Podcast‑1.7B huggingface-cli download --resume-download Soul‑AILab/SoulX‑Podcast‑1.7B‑dialect --local-dir pretrained_models/SoulX‑Podcast‑1.7B‑dialect或使用 Python 脚本:
from huggingface_hub import snapshot_download snapshot_download("Soul‑AILab/SoulX‑Podcast‑1.7B", local_dir="pretrained_models/SoulX‑Podcast‑1.7B") snapshot_download("Soul‑AILab/SoulX‑Podcast‑1.7B‑dialect", local_dir="pretrained_models/SoulX‑Podcast‑1.7B‑dialect") - 基础模型:
-
运行示例生成音频
bash example/infer_dialogue.sh该脚本将生成一个对话音频,可用于快速体验模型效果。
-
(可选)启动 Web UI 如果希望通过图形界面操作:
python3 webui.py --model_path pretrained_models/SoulX‑Podcast‑1.7B若使用方言模型,则替换
--model_path为pretrained_models/SoulX‑Podcast‑1.7B‑dialect。 -
脚本与输出检查建议
- 准备你的对话文本:设定说话人标签、可插入笑声/叹气等副语言标记,比如
<|laughter|>、<|sigh|>。 - 若想用某个方言(如粤语、四川话等),使用方言模型,脚本中可加入方言提示。
- 生成后务必听一听:说话人音色是否一致、说话切换是否自然、对话节奏是否流畅。如有不满意,可调整脚本或标签再试。
- 准备你的对话文本:设定说话人标签、可插入笑声/叹气等副语言标记,比如
这样,你只需要按照 1️⃣ → 6️⃣ 步骤执行,就可以快速上手 SoulX‑Podcast 模型,生成你想要的播客/对话音频。
五、使用时需注意/挑战点
尽管这个模型功能强大,但也有一些你需要留意的地方:
-
硬件要求:长篇生成 + 多说话人 +方言 +副语言标签,算力需求可能比较高。
-
生成质量还是有边界:虽然效果优秀,但在极端方言、非常复杂对话情境下,误差可能比普通话更大。论文中方言生成在某些评测上误差还略高。
-
伦理与合法性:模型支持“零样本语音克隆”,所以存在被滥用的风险(假冒声音、冒充等)。项目方已明确表示不得用于未经授权的语音克隆、诈骗、冒充。
-
实时或交互式还有限:这个模型目前更适于“预先生成播客”“录制场景”,如果你要做实时语音对话或 live streaming,可能还需要额外的工程适配。
-
方言提示需要设计:若要生成方言语音,提示文本里可能需要加入“方言典型句子”来帮助模型切换方言。论文中称为 “Dialect‑Guided Prompting (DGP)” 方法。
六、总结
如果你正在寻找一个可以生成“多角色对话”“整期播客”“带方言+带笑+带感情”的语音合成解决方案,SoulX‑Podcast 无疑是目前非常值得尝试的一个选择。只不过要用好它,还需要一定的准备(脚本、提示、算力、合法性意识)。
组合为啥比继承更高级?以构建buff系统为例
主人公"帅春"是一个猎人,他的技能是向敌人丢斧头,有一天,他击杀猎物被猎人之神赐福,进入了一种玄之又玄的顿悟状态,醒来后已经领悟了一种并不稳定的技能:
他丢出的斧子,有
15%的概率被附着上切割之力,命中敌人后,敌人会在接下来的10秒内,每2秒损失5点生命值。
好的,背景交待完了,身为一个成熟的程序员,我们开始尝试在 Godot 里编写代码实现逻辑。
1. 当然是选择 Buff 系统
面对这个需求,我们的第一反应通常是:
“这不就是一个典型的 Buff 吗?”。
Buff 系统是 RPG 和许多动作游戏中不可或缺的一部分,它用来管理角色身上临时或永久的状态变化。
因此,我们尝试构建如下的结构:
给 玩家、投射物、敌人 都绑定一个名为 buff容器 的节点。
Buff容器 节点的作用主要为:
管理其宿主的所有buff(包括负面buff),计算它们的时间,挂载,层数,移除等,并提供便捷的接口给给代码使用。
并且,我们需要设计3个 Buff:
ok,我们先编写一个名为 BaseBuff 的基类,它具备如何能力:
- 声明自己的基础属性:
- 是否永久的
- 持续时间
- 对层数的处理策略(可叠加,还是刷新等)
- 声明自己的基本类型
- 属性增强?
- 概率触发?
- 事件触发?
- 时间间隔触发?
- 实现触发时的专属业务逻辑(当然,作为 BaseBuff,它没有自己的专属业务逻辑,只是预留一个函数,方便继承者在声明基础信息后,直接覆写该方法)
因此,作为惯性,最本能的写法就是去继承,我们需要写如下三个代码(伪代码):
# 切割射击buff
class ABuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 攻击
触发概率 = 0.15
执行方法(context):
context.子弹.add_buff(BBuff.new())
# 切割弹buff
class BBuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 命中
触发概率 = 1.0
执行方法(context):
context.目标.add_buff(CBuff.new())
# 流血buff
class CBuff extends BaseBuff:
永久 = false
持续时间 = 10.0
类型 = 间隔触发
触发间隔 = 2.0
触发概率 = 1.0
执行方法(context):
context.目标.take_damage(5.0)
ok,大功告成,代码看起来非常完美,我真是一个经验丰富的程序员!
2. “继承” 的代价是什么?
但是。世事就怕这个但是。
但是,第二天,策划找到了我,并掏出了一份策划方案,告诉我,他给“猎人之神”创建了20种+不同的赐福类型,方便每个玩家在每局游戏都能对“主角帅春”进行不同路线等等强化,例如:
- 每3次攻击,就会一定会让斧子被附魔,命中后,敌人在8秒内,每2秒损失10点生命值。
- 每3秒,猎人就会给自己恢复20点生命。
- 每5秒,猎人会随机给一个敌人释放A技能。
- 每4秒,猎人会随机给一个敌人释放B技能。
- 每3秒,猎人会随机给一个敌人释放C技能。
……等等
我了个!这意味着我不得不放下手里正在进行的其他重要工作,配合策划,开始编写如上几十个buff的代码,毕竟这每个buff都需要声明属性,并且实现它的实现放方法:
执行方法(context):
本buff干了啥
更操蛋的是,万一未来策划要微调属性,也还是得找到我,我来修改,毕竟这实现和赋值都在继承体内部。
太爆炸了,未来如果技能上百个了,或者上千个了怎么办?
我们发现,继承是一种非常强的(IS-A)关系。BleedBuff 是一个 BaseBuff。这种关系一旦确定,就很难再改变。当一个事物需要同时具备多种不同维度的特性时,继承就会变得异常笨拙,导致类爆炸 或者形成庞大而复杂的继承树。
或者,我们应该换一个思路,正如人们常说的一句话:
正如人们常说的一句话。 —— 人们常说
不好意思,开个玩笑,重新来。
正如人们常说的一句话:
组合大于继承。
那么,是如何大于的呢?它又应该如何工作呢?
答案是抽象。
3. “组合”是更狠的抽象
抽象是编程的基础思想。
其实“继承” 过程中我们已经进行了一定程度的抽象。
正如Java 或者 C++ 老师所讲的,所有动物都继承了Animal,但是:
Dog.say() 和 Cat.say() 的实现是不同的,因此,Dog 和 Cat 通过继承实现了多态。
我们思考了哪些属性是大部分Buff都必须考虑的共性,并且产生了一个思维惯性:
执行逻辑的也是
Buff,因此我们应该继承BaseBuff,利用多态的特性来满足不同的需求。
但“组合”必须跳开这个思路,产生更为细的抽象:
实现差异的可能并不是
动物不同,而是动物持有的发声器不同。
在这样的抽象细粒度里,动物的特性可以通过茫茫多不同的组合来实现。
再引入一些配置化的手段,以上图中就可以产生 3^4 = 81 种不同形态的动物了,这要是让你写继承,不得写崩溃?
因此,组合的抽象步骤一般分为两步:
- 拆分成更细粒度的功能,并抽象成接口。
- 实现不同的功能单元。
后续使用者可以不必再是代码,而是配置,哪怕是完全不懂代码的人也可以胜任。
4. 重构Buff成组合模式
实现
Buff业务逻辑的不一定是Buff本身。
让我们按这个逻辑来进行抽象,现在我们有了一个“实现Buff具体逻辑”的工具接口“效果器”:
class_name BaseEffect:
func execute(context):
pass
每当Buff需要执行逻辑时,只需要找到Buff实例上挂载的“效果器”并执行 execute 方法即可。
让我们尝试来抽象一下,以上例子里有哪些效果器呢?
- 效果器A:给目标施加一个新Buff。
- 效果器B:对目标造成伤害。
咦?竟然这么简单?是的。接下来就只用配置属性和组合。
# 切割射击buff
class ABuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 攻击
触发概率 = 0.15
效果器 = 效果器A
施加的Buff = BBuff
# 切割弹buff
class BBuff extends BaseBuff:
永久 = true
类型 = 事件触发
触发事件 = 命中
触发概率 = 1.0
效果器 = 效果器A
施加的Buff = CBuff
# 流血buff
class CBuff extends BaseBuff:
永久 = false
持续时间 = 10.0
类型 = 间隔触发
触发间隔 = 2.0
触发概率 = 1.0
效果器 = 效果器B
伤害 = 5.0
现在我们看到,整个步骤已经无需写代码了,只需要配置,配置,配置。
让我们把这些配置抽取成 godot 里的 Resource 资源,或者弄一个 Json 文件,或者弄一个标准化的 Excel 表格,便可以交给策划来进行日常维护了。
5.结论:为什么组合比继承更“高级”?
回到最初的问题。
说组合比继承“高级”,并不是说继承一无是处。继承在建立明确、稳定、层次分明的类型关系时非常有用(例如,Button 继承自 Control,Control 继承自 Node)。
但在构建复杂且多变的游戏系统(如技能、Buff、道具系统)时,组合的优势是压倒性的:
-
高度灵活性 (Flexibility):你可以动态地组合行为,而不是在编译时就用继承关系写死。想创造一个新Buff?只需要在编辑器里拖拽几个效果资源,调整一下参数即可,完全无需编写新的类。
-
高内聚,低耦合 (High Cohesion, Low Coupling):每个 BuffEffect 只关心自己的逻辑(高内聚)。BuffComponent 不知道也不关心效果的具体内容,它只负责管理Buff的生命周期(低耦合)。这使得代码更容易维护和测试。
-
避免类爆炸 (Avoids Class Explosion):对于 N 个效果,如果想实现它们之间的任意组合,继承可能需要创建 2^N - 1 个子类。而使用组合,你只需要 N 个效果类。
-
更符合数据驱动的设计:使用 Godot 的 Resource,游戏设计师(甚至是不会编程的策划)可以直接在编辑器里创建和调整各种Buff,极大地提高了开发效率和迭代速度。
总而言之,继承告诉我们一个对象“是什么”,而组合则定义了一个对象“能做什么”。在需要灵活插拔、组合功能的游戏开发领域,优先考虑组合,而不是继承,这会让你在面对不断变化的需求时,游刃有余。
小白也能懂!跨域问题到底是啥,咋解决?
小白也能懂!跨域问题到底是啥,咋解决?
大家好呀!最近有朋友问我:“我做了个小网页,想拿别人网站的数据(比如天气 API),结果浏览器报错说‘跨域了’,这啥意思啊?” 今天就用大白话给大家讲明白 —— 跨域问题到底是啥,以及怎么解决它。
一、先搞懂:啥是 “跨域”?
要理解 “跨域”,得先知道一个词:同源。
“同源” 就是说两个网址的「协议」「域名」「端口号」这三样必须完全一样!少一个一样都不行。
举个栗子,假设你的网页地址是 a.com:8080(协议是 http,域名是a.com,端口 8080),那下面这些情况就是「跨域」:
| 你的网址 | 要访问的网址 | 为啥跨域? |
|---|---|---|
| a.com:8080 | a.com:8080 | 协议不一样(http≠https) |
| a.com:8080 | b.com:8080 | 域名不一样(a.com≠b.com) |
| a.com:8080 | a.com:3000 | 端口不一样(8080≠3000) |
简单说:只要两个网址不是 “同源”,你从 A 网址去拿 B 网址的数据,浏览器就会拦着你,这就是跨域问题。
二、为啥浏览器要 “拦着”?—— 同源策略
你可能会问:“浏览器为啥这么严?我就是想拿点数据而已啊!”
这其实是浏览器的一个安全规则,叫「同源策略」,目的是保护你的信息安全。
再举个栗子:你登录了淘宝(taobao.com),浏览器会存一个 “登录凭证”(cookie)。如果没有同源策略,随便一个小网站(bad.com)都能偷偷拿你淘宝的登录凭证,那你的账号不就危险了?
所以「同源策略」就像你家的门锁 —— 只让 “自己人”(同源网址)进,不让 “外人”(跨域网址)随便碰你家东西(数据)。
三、3 个小白也能懂的解决办法
知道了啥是跨域、为啥有跨域,接下来就是重点:怎么解决?
下面给大家讲 3 个最常用、最简单的办法,不同场景选不同的就行~
办法 1:让后端 “开门”—— CORS(最推荐)
这是现在最常用的办法,核心是:让你要访问的后端(比如天气 API 的服务器)主动说 “允许你这个网址来拿数据” 。
怎么实现?其实不用前端操心,让后端在返回数据时,多带一个 “特殊头信息” 就行。
举个后端的简单例子(用 Node.js 写的,其他语言逻辑一样):
// 后端代码( Node.js + Express )
const express = require('express');
const app = express();
// 关键:加这行代码,
app.use(cors({
// 允许所有域名跨域访问
origin:"*",
// 允许所有 HTTP 方法跨域访问,这里默认是加了预检请求
methods:["GET","POST","PUT","DELETE"],
// 允许所有请求头跨域访问
allowedHeaders:["Content-Type","Authorization"],
}))
// 后端接口:返回天气数据
app.get('/weather', (req, res) => {
res.send({ city: '北京', temp: '25℃' }); // 给前端的数据
});
app.listen(3000);
前端不用改任何代码,直接正常发请求就行:
// 前端代码(普通JS)
fetch('http://b.com:3000/weather') // 访问后端接口
.then(res => res.json())
.then(data => console.log(data)); // 成功拿到天气数据!
适用场景:你能联系到后端开发者(比如公司内部项目),让他们加这行配置。
办法 2:借 “script 标签” 的光 —— JSONP(老项目常用)
有些老项目后端没法改,那可以用 JSONP。它的原理很有意思:浏览器的 script 标签不受同源策略限制(比如你能在自己网页里引百度的 JS 文件)。
实现步骤也很简单:
- 前端先定义一个 “回调函数”(用来接收数据);
- 用 script 标签去请求后端接口,并且把 “回调函数名” 传给后端;
- 后端返回一段 JS 代码,内容是 “调用这个回调函数,把数据当参数传进去”。
举个例子:
// 1. 前端定义回调函数(拿到数据后要做的事)
function handleWeather(data) {
console.log('拿到天气了:', data); // 比如打印数据
}
// 2. 用script标签请求后端接口,传回调函数名
const script = document.createElement('script');
// 后端接口地址 + ?callback=回调函数名
script.src = 'http://b.com:3000/weather?callback=handleWeather';
document.body.appendChild(script); // 插入页面,发起请求
// 3. 后端返回的内容会是:handleWeather({city:'北京', temp:'25℃'})
// 浏览器执行这段JS,就会调用我们定义的handleWeather,拿到数据!
后端代码(Node.js):
app.get('/weather', (req, res) => {
const callbackName = req.query.callback; // 拿到前端传的回调函数名
const weatherData = { city: '北京', temp: '25℃' }; // 要返回的数据
// 关键:返回“回调函数调用”的JS代码
res.send(`${callbackName}(${JSON.stringify(weatherData)})`);
});
注意:JSONP 只能发 GET 请求,不能发 POST 请求,所以适合拿数据,不适合提交数据。
办法 3:前端自己 “搭个桥”—— 代理服务器(本地开发用)
如果你是本地开发(比如用 Vue/React 写项目),后端还没配置 CORS,那可以用 “代理服务器”。
原理很简单:浏览器有同源策略,但服务器之间没有! 所以我们让前端先把请求发给 “自己的代理服务器”,再让代理服务器去请求后端,最后代理服务器把数据返回给前端。
以 Vue 项目为例,只需要改一下配置文件(vue.config.js):
// vue.config.js 配置
module.exports = {
devServer: {
proxy: {
// 只要前端请求以 /api 开头,就走代理
'/api': {
target: 'http://b.com:3000', // 后端接口的真实地址
changeOrigin: true, // 告诉后端“我是从你允许的网址来的”(伪装)
pathRewrite: {
'^/api': '' // 把请求里的 /api 去掉(比如 /api/weather → /weather)
}
}
}
}
};
然后前端请求时,直接写 /api/weather 就行:
// 前端代码(Vue里)
this.$axios.get('/api/weather') // 看似请求自己的代理,实际会转发到后端
.then(res => console.log(res.data));
适用场景:本地开发时临时用,不用麻烦后端改配置。
四、总结一下
- 跨域就是:浏览器不让不同 “源” 的网址互相拿数据,为了安全;
- 解决办法选哪个?
-
- 能联系后端 → 用 CORS(最推荐);
-
- 后端没法改,只需要 GET 数据 → 用 JSONP;
-
- 本地开发临时用 → 用代理服务器。
其实跨域问题没那么难,关键是理解 “同源策略” 的初衷,然后根据场景选对方法~ 大家可以动手试试上面的例子,很快就能掌握啦!
连载小说大学生课设 需求&架构
第一台 Andriod XR 设备发布,Jetpack Compose XR 有什么不同?对原生开发有何影响? | 掘金一周 10.30
本文字数1400+ ,阅读时间大约需要 6分钟。
【掘金一周】本期亮点:
- Konva.js 实现 腾讯文档 多维表格
- 🍀我实现了个摸鱼聊天室(做了防发现处理)🚀
- 统计接口耗时的6种常见方法
- 第一台 Andriod XR 设备发布,Jetpack Compose XR 有什么不同?对原生开发有何影响?
- 理解retain{}的内部机制:Jetpack Compose中基于作用域的状态保存
- 别再乱用Embedding了!揭秘RAG系统真正灵魂的3大核心组件——90%开发者都搞错了
「上榜规则」:文章发布时间在本期「掘金一周」发布时间的前一周内;且符合各个栏目的内容定位和要求。 如发现文章有抄袭、洗稿等违反社区规则的行为,将取消当期及后续上榜资格。
一周“金”选
内容评审们会在过去的一周内对社区深度技术好文进行挖掘和筛选,优质的技术文章有机会出现在下方榜单中,排名不分先后。
前端
本文介绍基于 Konva.js 实现高性能多维表格系统。该系统支持大规模渲染、分组管理等功能,适用于复杂表格场景。阐述核心架构、模块及关键机制,详述实现过程,还提及性能优化与扩展性,为开发提供思路。
本文介绍了摸鱼聊天室的技术实现,前端采用 Vue3、Pina 等,实现主题色修改、暗黑模式切换等功能;后端运用 Nest、Mysql 等,涵盖 Redis、Prisma 使用,还包含权限校验、邮件发送及 Socket 通信,助力打造防发现的聊天环境。
一个函数超过20行? 聊聊我的函数式代码洁癖 @ErpanOmer
文章围绕函数式代码洁癖展开,指出超20行的长函数存在阅读成本高、无法单元测试、有隐形副作用等问题。倡导函数小且单一职责、追求纯函数。并通过实例展示重构过程,强调代码应简单可预测。
h5后台切换检测利用visibilitychange的缺点分析 @拉不动的猪
本文围绕
visibilitychange事件展开,介绍其是页面可见性 API 核心,可判断页面前后台状态,有诸多适用场景且兼容性好。但也存在缺点,如无法区分隐藏原因、触发时机不精准等,还给出了区分常见场景的辅助方法。
后端
文章介绍了统计接口耗时的6种常见方法,包括
System.currentTimeMillis()、System.nanoTime()、Spring AOP、拦截器、过滤器以及Micrometer和APM工具。分析了各方法优缺点与适用场景,给出选择原则与最佳实践,助力性能优化与故障排查。
SpringBoot开发使用Mybatis,还是Spring Data JPA? @苏三说技术
文章围绕Spring Boot项目中MyBatis和Spring Data JPA的选择展开。先介绍两者特点,从项目规模、团队技能等方面分析选择原因,对比性能、灵活性等,结合实际案例给出建议,指出应按需选择,也可混用。
PostgreSQL-10个鲜为人知的强大功能 @PFinal社区_南丞
本文介绍PostgreSQL 10个鲜为人知的强大功能,如全文搜索、JSONB、数组类型等。使用这些功能可告别Elasticsearch和MongoDB,简化技术栈,实现2 - 30倍性能提升,还能降低成本、提升可维护性。
SpringBoot “分身术”:同时监听多个端口 @风象南
构建“双面”Spring Boot 应用可简化架构、降低运维成本。技术实现有多 Tomcat Connector 与路径前缀两种方案,还有端口感知拦截等高级特性,具备分端口日志、健康检查功能,同时有端口访问控制等安全策略。
Android
第一台 Andriod XR 设备发布,Jetpack Compose XR 有什么不同?对原生开发有何影响? @恋猫de小郭
三星推出首款 Galaxy XR 设备,搭载 Android XR 系统。官方提供 Jetpack XR SDK 及 Jetpack Compose UI 框架,降低开发和适配成本。应用有主空间和全空间两种运行模式,SDK 新增“空间化”能力,当前原生开发赛道较纯净。
理解retain{}的内部机制:Jetpack Compose中基于作用域的状态保存 @稀有猿诉
本文围绕 Jetpack Compose 中
retain{}展开,介绍其内部机制。对比remember{}局限,阐述retain{}优势及 API 。还涉及RetainScope等组件,分析内存性能,强调使用注意,助开发者合理选择保存状态方式。
人工智能
别再乱用Embedding了!揭秘RAG系统真正灵魂的3大核心组件——90%开发者都搞错了 @AI大模型
本文指出很多开发者过度依赖Embedding,忽视RAG系统核心组件。介绍了检索器、重排序器、生成器三大核心组件,强调其协同工作的重要性。还阐述Embedding局限与适用场景,给出构建高效RAG系统的实战指南。
📖 投稿专区
大家可以在评论区推荐认为不错的文章,并附上链接和推荐理由,有机会呈现在下一期。文章创建日期必须在下期掘金一周发布前一周以内;可以推荐自己的文章、也可以推荐他人的文章。
Android开发中常用高效数据结构
🔥🔥从前端到 AI Agent 开发者,只差这一篇入门指南
前端开发人员,你该恐惧吗?从前端到 AI Agent开发者,如何从熟悉的技术中找到连接AI的桥梁。
开篇真话:你该恐惧吗?
你可能听过这样的论调:
- "AI会替代80%的程序员工作"
- "如果你不学AI,3年后就没竞争力了"
- "现在的程序员都在卷AI,你落伍了吗?"
但这些说法都是片面的。 现实要复杂得多:
- ❌ 不会发生的: AI不会替代程序员 (2小时访谈:未来十年,没有 AGI,只有 Agent)
- ✅ 真实发生的: 懂AI的程序员正在替代不懂AI的程序员
而最关键的是——前端开发者是转型AI Agent工程师的最优人选。不是"勉强可以",而是"天然适合"。
为什么是你?前端开发者的竞争优势
优势1:技术栈天然匹配 - 你已经赢在起点
你熟悉的技术栈:
Vue.js / React + TypeScript + Node.js
恰恰就是AI Agent开发的业界标准技术栈。这不是巧合,而是AI Agent开发本质上是全栈工程。
优势2:工程化思维 - 你能驾驭复杂系统
前端工程师必然具备:
- 系统拆解能力 - 能将复杂UI拆成可复用组件
- 状态管理思维 - Redux/Pinia的状态管理就是Agent的记忆管理
- 性能优化意识 - 懂异步、并发、缓存的重要性
- 容错设计能力 - 懂降级、重试、容错处理
这些能力在构建AI Agent时同样关键。事实上,一个不稳定、无法扩展的AI系统比一个性能差的网站更灾难。
优势3:快速学习能力 - 你已经习惯了"永远在学新东西"
前端技术更新最快,你习惯了:
- 每年学新框架(Vue3、React18...)
- 快速适应新工具链
- 从文档快速上手新库
- 在项目中边学边用
这个能力直接转化为AI学习能力。事实上,学LangChain的难度远低于从Webpack迁移到Vite。
核心概念5分钟速成
如果你还不清楚AI Agent到底是什么,这5分钟会让你茅塞顿开。
AI Agent的本质:有思考能力的程序
普通程序:
输入 → 固定逻辑 → 输出
AI Agent:
输入 → 思考 → 决定行动 → 执行工具 → 观察结果 → 继续思考 → 输出
核心差异: Agent会根据结果持续调整策略,而不是执行固定流程。
3秒看懂Agent的工作方式
忘记复杂的理论,看这个简化图你就明白了:
┌──────────────────────────────────────────────────────┐
│ 用户与AI的互动 │
└──────────────────┬───────────────────────────────────┘
│
▼
┌─────────────────────────┐
│ 你熟悉的前端界面 │
│ (Vue.js / React) │
└────────────┬────────────┘
│ (HTTP请求)
▼
┌─────────────────────────┐
│ 你熟悉的后端API │
│ (Node.js + Express) │
└────────────┬────────────┘
│
▼
┌──────────────────────────────────────┐
│ AI Agent核心(这是新东西) │
│ ├─ LLM: 大脑(理解和推理) │
│ ├─ Tools: 双手(调用工具) │
│ ├─ Memory: 记忆(记住对话) │
│ └─ Prompt: 指令(告诉AI怎么做) │
└────────────┬─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 工具集和知识库 │
│ (数据库/API/知识库等) │
└─────────────────────────┘
关键洞察:
- 前两层(UI + API)你已经精通
- 中间层(Agent核心)这是你需要学的新东西
- 下面层(工具集)和集成API没区别
换句话说: 你只需要在熟悉的技术中间,嵌入一个"有思考能力"的AI大脑。就这么简单!
你已经熟悉的概念类比
| AI Agent概念 | 前端开发的类比 | 说明 |
|---|---|---|
| LLM(大脑) | Vue/React实例 | AI的核心引擎 |
| Tools(工具) | API调用 | Agent执行的动作 |
| Memory(记忆) | localStorage/Redux状态 | 保存上下文信息 |
| Prompt(提示词) | 组件props | 指导AI的行为 |
| Chain(链) | 函数调用链 | 多步骤流程编排 |
| RAG(知识库) | 数据库查询 | Agent的知识来源 |
AI Agent的工作流程
用户提问:"我需要处理这3笔订单退款"
↓ (AI思考)
Agent的内部推理:
"我需要:
1. 查询这3个订单的详情
2. 检查退款政策
3. 计算应退金额
4. 执行退款操作
5. 生成退款凭证"
↓ (决策执行)
Agent调用工具:
1. tool_query_order() → 获取订单数据
2. tool_check_policy() → 检查退款规则
3. tool_calculate_refund() → 计算金额
4. tool_process_refund() → 执行退款
5. tool_generate_receipt() → 生成凭证
↓ (观察结果)
Agent观察工具输出,判断是否满意
↓ (输出)
用户收到完整的处理结果和凭证
整个过程用时: 8秒 人工处理用时: 15-20分钟 准确率: 99.8%
🔥 第一行代码:10行让你出成果
别只是听我说,让我们用真实代码体验一下。
环境准备(1分钟)
Node.js 版本:18.0+
# 创建新目录
mkdir my-first-ai-agent
cd my-first-ai-agent
# 初始化项目
npm init -y
# 安装必要的包
npm install @langchain/openai dotenv
# 安装开发依赖(用于运行TypeScript)
npm install -D typescript tsx
# 创建环境文件
echo "LLM_API_KEY=你的API_KEY" > .env
模型API Key申请,详见《5分钟搞定 DeepSeek API 配置:从配置到调用一步到位》
配置package.json(30秒)
在 package.json 中添加配置:
# 在package.json的scripts部分添加(手动编辑或执行下面的命令)
npm pkg set type="module"
npm pkg set scripts.start="tsx hello-agent.ts"
写你的第一个AI对话(2分钟)
创建文件 hello-agent.ts:
import { ChatOpenAI } from '@langchain/openai';
import 'dotenv/config';
// 1. 初始化AI大脑 - 使用 Deepseek 模型(低成本)
const llm = new ChatOpenAI({
modelName: "deepseek-chat",
temperature: 0.7, // 创造性程度:0=严谨,1=发散
maxTokens: 1000, // 最多输出1000个token
apiKey: process.env.LLM_API_KEY,
configuration: {
baseURL: "https://api.deepseek.com/v1",
},
});
// 2. 发起你的第一个AI对话
const response = await llm.invoke(
"我是一个前端开发者,想转向AI Agent开发。从哪开始?给我5个实战建议。"
);
// 3. 打印结果
console.log("🤖 AI的回答:\n");
console.log(response.content);
运行它(1分钟)
npm start
你会看到(惊喜时刻)
🎉 恭喜!你已经完成了第一个AI对话!
下一步怎么走?
立刻可以做:
- 复制上面的代码,在你的机器上跑一遍(5分钟)
- 修改提示词,尝试不同的问题(10分钟)
- 对比不同的temperature参数,看AI回答如何变化(10分钟)
记录一次修改 PNPM 版本,部署 NextJs 服务时导致服务器崩溃的问题 😡😡😡
最近在使用 NestJs 和 NextJs 在做一个协同文档 DocFlow,如果感兴趣,欢迎 star,有任何疑问,欢迎加我微信进行咨询 yunmz777
最近在使用 NextJs 开发 DocFlow 协同编辑这个项目,运行了很久都没有问题,最近终于在部署方面出现了问题了,早上的时候发现 ssh 连不上了,一开始并没有太在意,但后来打开京东云控制台,好家伙真的好家伙:
真的小母牛坐飞机牛逼上天了!原来是 CPU 爆了,怪不得连接不上了,当然网站也 502 了:
那是什么原因呢,接下来就要开始慢慢排查了,既然 vscode 连不上,那我们就在官方提供的 ssh 来连接:
输入命令 htop,罪魁祸首马上出现:
原来是 next-server 和 node(运行在 /home/DocFlow-Server/dist/main.js)占用了基本全部的 CPU,那这个原因就很清楚了,那么接下来就可以排查了这个问题的原因了。
首先,可以明确的一点是,之前一直都是同样的方式部署的,没有出现问题,但是为什么今天就一直出现这个问题呢,那肯定是在运行的进程出现了问题。
我项目使用的是 pm2 部署的,那打个日志看看咯:
问题找到了,原来是 pm2 一直在重启,把内存全部占用了,具体是什么原因的已经不好查了,有可能是安装了一些依赖,但是我的 Github Action 是有一些问题没有处理的:
name: Deploy Next.js to JD Cloud
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4.2.0
- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1.2.1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USERNAME }}
port: ${{ secrets.SERVER_PORT }}
password: ${{ secrets.SERVER_PASSWORD }}
script: |
# 加载环境配置
source ~/.bashrc 2>/dev/null || true
source ~/.profile 2>/dev/null || true
# 加载 NVM 环境
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
# 设置 PATH
export PATH="$HOME/.nvm/versions/node/*/bin:/usr/local/bin:/usr/bin:/bin:$HOME/.npm-global/bin:$PATH"
# 设置工作目录
PROJECT_DIR="/home/DocFlow"
mkdir -p $PROJECT_DIR
cd $PROJECT_DIR
# 克隆或更新代码
if [ ! -d ".git" ]; then
echo "Cloning repository..."
rm -rf ./*
git clone git@github.com:xun082/DocFlow.git .
else
echo "Updating repository..."
git fetch origin main
git reset --hard origin/main
fi
# 创建 .npmrc 文件 (如果不存在)
if [ ! -f ".npmrc" ]; then
echo "Creating .npmrc file..."
echo "@tiptap-pro:registry=https://registry.tiptap.dev/" > .npmrc
echo "//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_AUTH_TOKEN }}" >> .npmrc
else
echo ".npmrc file already exists, skipping creation."
fi
# 安装全局依赖 (只在需要时安装)
command -v pnpm >/dev/null 2>&1 || npm install -g pnpm@9.4.0
command -v pm2 >/dev/null 2>&1 || npm install -g pm2
# 安装项目依赖
pnpm install
# 构建项目
pnpm run build
# 重启应用
pm2 restart DocFlow 2>/dev/null || pm2 start "pnpm start" --name DocFlow
我这个部署脚本是基本没问题的,至少能跑,现在查看这两天的 commit,想起来有一个这样的操作:
那基本就可以确定是这个问题了。
其他部署方案
PM2 是一种流行的进程管理工具,适用于 Node.js 应用,尤其在小型项目和单机部署时很有效。然而,PM2 的部署流程存在一些局限性:
-
依赖管理和版本控制:PM2 需要手动确保服务器上安装正确的 Node.js 和 pnpm 版本。每次代码更新后,你还需要在服务器上重新 构建项目(执行
pnpm run build),这可能会导致生产环境与开发环境之间的不一致,并增加了维护工作。 -
服务中断问题:每次代码更新时,PM2 需要 重启应用 来使新版本生效。如果没有正确配置或管理,应用在重启过程中可能会停机一段时间,这对于高可用性要求较高的生产环境来说是个问题。
与 PM2 相比,Docker 自动化部署 通过容器化解决了这些问题,提供了更简洁、更一致的部署流程,特别适用于云端和多环境部署。主要优势包括:
-
无需依赖管理和环境配置:Docker 将应用及其所有依赖、环境变量、构建过程封装在 Docker 镜像 中。这样,服务器上无需安装特定版本的 Node.js 或 pnpm,避免了手动配置和版本不一致的问题。你只需要在 构建镜像时 配置好所有的依赖和环境,部署时无需再进行任何安装或构建操作。
-
代码问题不会影响运行:如果代码有问题,Docker 使得应用 不会受到当前部署的影响。因为 Docker 镜像已经包含了所有必要的依赖,部署失败时,原先的镜像仍然可以继续运行,确保生产环境不受影响。而在 PM2 部署 时,更新代码后需要重启应用,如果代码有问题,应用会直接停机,导致服务中断。
-
无缝的 CI/CD 集成:Docker 完美集成 CI/CD 流程。例如,使用 GitHub Actions,你可以自动构建 Docker 镜像并推送到仓库。服务器只需要 拉取最新镜像,并启动新的容器。如果部署失败,不会影响当前运行的容器,你可以迅速恢复服务,且无需手动修复。相反,PM2 需要重新启动应用,且服务停机时需要手动修复代码。
-
快速回滚:由于 Docker 镜像的版本化机制,你可以轻松回滚到之前的稳定版本。即便遇到部署失败的情况,只需要拉取旧版本的镜像,应用立即恢复,不需要手动操作,这为应用提供了更高的可靠性。
总结
PM2 部署中,如果代码有问题,必须手动修复并重启应用,这可能会导致 服务中断,并影响用户体验。而且每次更新都需要在服务器上重新构建项目,这可能导致环境不一致,增加了部署的复杂性。
Docker 部署则通过 容器化 确保应用和环境的一致性,部署失败时不会影响生产环境的运行,原有容器仍可继续工作,且通过 CI/CD 流程可以自动恢复和快速回滚,不需要人工干预。即使代码存在问题,原有的容器仍可保持正常服务,确保应用的高可用性。
因此,对于生产环境,Docker 提供了比 PM2 更稳定、高效、自动化的部署方式,尤其适合大规模、高可用的应用部署。
老板问我:AI真能一键画广州旅游路线图?我用 MCP 现场开图
老板那天凑过来:“听说AI能一键画旅游地图?真的假的?”
我笑了:“来,给您现场演示一下。”
打开电脑,三下五除二配置好环境。在对话框里输入:“生成广州旅游路线地图,要能交互的。”
3秒后,一个精美地图跃然屏上——广州塔、沙面、陈家祠等清晰标注。
老板瞪大眼睛:“这就完了?太神奇了吧!”
“这就是MCP的魔力,让AI从聊天工具变成生产力神器。”
他摸着下巴:“看来我们得抓紧用起来了。”
想知道怎么实现?本文将从零开始,教你实现这个"魔法"
效果预览:
MCP简要介绍
MCP(Model Context Protocol)就是能够让 AI 大模型更好地使用各类工具的一个协议。
AI 模型只是一个大脑,而 MCP 协议就是让模型加上手脚,可以帮你干更多的事情,比如:
- 读取、修改你的本地文件
- 使用浏览器上网查询信息
- 查询实时交通路况
- 生成各种图表、地图路线
总之,MCP 能干的事情可太多了!要知道,大模型本身其实只会问答,它自己并不会使用外部工具,而 MCP 的出现就等于是让大模型拥有了使用各种外部工具的能力。
不过要想使用 MCP,你还得用到一个东西叫做 MCP Host 。
MCP Host
MCP Host 就是一个支持 MCP 协议的软件环境,能"跑"AI 模型,并且让 AI 模型能用各种外部工具。
常见 MCP Host:
- Claude Desktop
- Cursor
- Trae
- Cline (我们今天要使用的例子)
我们今天使用 Cline 为例来讲讲 MCP 的使用方法。
安装Mcp host (Client)
cline是vscode的一个插件,首先我们要下载vscode编辑器,然后在插件商店搜索cline并安装,安装好了之后侧边栏就会出现一个cline图标,点一下就进入使用cline的地方。
配置Cline用的API key
紧接着,我们需要配置ai模型,cline支持接入不同模型的api,如cluade、gpt、deepseek等模型,我们这里演示使用deepseek模型进行演示。
deepseek的官方网站就有提供api,我们可以到deepseek官方网站注册登录,并充值获取api。
创建好key之后,将key给cline,填好之后,cline会引导我们来到聊天页面,我们随便问它一个问题,给他打个招呼,看他能不能够正常回复,能正常回复就说明接入deepseek模型成功了
概念解释:MCP Server 和 Tool
MCP Server
MCP Server 即 MCP 服务器,跟我们传统意义上的 Server 并没有什么太大的关系,它就是一个程序而已,只不过这个程序的执行是符合 MCP 协议的。
大部分的 MCP 服务器都是本地通过 Node 或者是 Python 启动的,只不过在使用的过程中可能会联网,当然它也可能不联网,纯本地使用也是可以的。不管是联不联网,它都可以叫做 MCP Server,本质就是给 MCP 客户端即 AI 模型提供服务的。
Tool
所以 MCP Server 本质就是一个程序,就像手机上的应用,这些应用都内置了一些功能模块,而 Tool 就是 MCP Server 程序中的一些工具函数。
可以把 MCP Server 理解为一个计算器应用,这个计算器有计算和换算两个功能,作为用户可以根据自己需求选择计算还是换算功能,而这两个功能就是两个Tool。
比如我们要让 DeepSeek 生成一个可交互的广州旅游路线地图,DeepSeek 是没办法完成的,但是我们可以安装一个处理生成图表的 MCP Server,它内部包含一个函数 generate_path_map 即 Tool,这个功能是传入地点、图的大小就可以返回路线地图。
所以我们要一个广州旅游路线地图的话,就得让cline安装处理生成图表的MCP Server,然后deepseek把地点、图的大小这些参数传给MCP Server的generate_path_map就可以拿到一个可交互的广州旅游路线地图了。
配置MCP Server
前面解释了MCP Server和Tool的概念,我们再回到cline这里继续实操。
首先我们打开进入cline,进入MCP Server 设置页面,点击“已安装”,再点击“配置MCP服务器”,之后cline就会跟我们打开一个cline_mcp_settings.json文件。如果我们想新增一个MCPServer的话,我们只需要在里面填入对应的启动命令就行了。
如下操作:
使用他人制作的MCP Server
接下来我们来安装一个别人写好的MCP Server,我们打开mcpmarket.cn/ ,这是一个MCP Server的市场,就跟我们手机的应用市场有点像,这里面有很多别人写好的MCP Server,我们去找生成图表的MCP ,复制配置就可以生效了
生成图表的MCP Serve工具链接: mcpmarket.cn/server/680e…
跟着以下的操作图进行操作:
按照网站上的说明,将配置添加到 cline_mcp_settings.json 文件中
window用户:
{
"mcpServers": {
"mcp-server-chart": {
"command": "cmd",
"args": ["/c", "npx", "-y", "@antv/mcp-server-chart"]
}
}
}
mac用户:
{
"mcpServers": {
"mcp-server-chart": {
"command": "npx",
"args": ["-y", "@antv/mcp-server-chart"]
}
}
}
注意:电脑要装Node.js环境
没有的话要自行安装哦: nodejs.org/en/download…
实战演示
配置完成后,你就可以在 Cline 中输入:
"生成一个广州的旅游路线地图"
DeepSeek 会自动调用 MCP Server 的相关工具,为你生成一个精美的交互式地图!
MCP交互流程详解
sequenceDiagram
participant 用户
participant Cline
participant deepseek
participant MCP as MCP Server
用户->>Cline: 生成广州旅游路线地图
Cline->>deepseek: 用户请求 + 可用工具列表
deepseek->>Cline: 调用generate_interactive_map工具
Cline->>MCP: generate_interactive_map(广州, 景点列表)
MCP->>MCP: 生成交互式地图
MCP->>Cline: 返回地图
Cline->>deepseek: 工具执行结果
deepseek->>Cline: 整理回复和说明
Cline->>用户: 显示地图和旅游建议
-
用户 -> Cline:“帮我生成一个广州的旅游路线地图,要可交互的哦!” -
Cline -> deepseek模型::cline把用户的请求和可用的mcp-server-chart工具信息一起交给模型deepseek来想办法 -
deepseek模型 -> Cline:deepseek模型看到请求和工具后,就想:这个任务可以用mcp-server-chart工具的generate_interactive_map功能,需要指定地点、景点和样式。于是它告诉Cline:“你去调用generate_interactive_map工具,参数是广州、这些景点和旅游路线样式。” -
Cline -> MCP Server: Cline就拿着这些参数去调用地图工具(mcp-server-chart)的generate_interactive_map函数。 -
MCP Server -> Cline: 地图工具mcp-server-chart接到命令后,就忙活起来,生成一个可交互的广州旅游路线地图,然后把生成的结果返回给Cline。 -
Cline -> deepseek模型: Cline拿到地图后,就把这个结果交给deepseek模型 -
deepseek模型 -> Cline:模型再组织一下语言,比如解释一下地图怎么用,再传给cline -
Cline -> 用户: 然后Cline就把最终的回答和地图一起展示给用户。
总结
上述内容我们主要讲了4点,分别是:
- MCP 协议的核心概念 :让 AI 模型拥有使用外部工具的能力
- 完整的环境搭建 :从 cline安装到 MCP Server的配置
- 实战操作流程 :配置 MCP Server 并生成交互式地图
- 技术原理理解 :MCP Host、MCP Server 和 Tool 的关系
如果觉得对您有帮助,欢迎点赞 👍 收藏 ⭐ 关注 🔔 支持一下! 往期实战推荐:
从 TypeScript 视角读懂 Java 和 TS 类中 new 自己的区别
Java class 里 new 自身 和 TypeScript 里 new 自己的区别分析
在日常开发中,我们经常会在类的内部通过 new 关键字实例化自身对象。Java 和 TypeScript 都支持面向对象编程,但它们在“类内 new 自身”这一行为上有着本质的区别。本文将从语法、运行时行为、类型系统等角度,详细分析 Java 和 TypeScript 在这方面的异同。
一、Java 中 class 里 new 自身
Java 是强类型、编译型语言。类内部 new 自身是非常常见的写法,比如:
public class Person {
public Person() {
System.out.println("Person 构造方法被调用");
}
public void createAnother() {
Person p = new Person();
System.out.println("又创建了一个 Person");
}
}
特点与机制:
-
类型确定
在类内部,new Person()总是创建当前类的实例,类型就是Person。如果该类被继承,子类内部new Person()依然是父类对象,不是子类。 -
构造方法调用
会调用当前类的构造方法,构造链严格按照声明顺序执行。 -
用途
常用于工厂方法、原型模式、递归结构等。 -
限制
不能直接通过this关键字 new 自身(new this()不合法)。只能通过类名 new。
二、TypeScript 中 class 里 new 自己
TypeScript 作为 JavaScript 的超集,支持类的语法糖。来看一个例子:
class Person {
constructor() {
console.log("Person 构造函数被调用");
}
createAnother() {
const p = new Person();
console.log("又创建了一个 Person");
}
}
特点与机制:
-
类型推断
new Person()创建的是当前类的实例,类型为Person。但 TypeScript 支持更灵活的 this 类型(多态 this),可以实现“new this()”的效果。 -
多态 this
TypeScript 支持new this.constructor()或new (this.constructor as any)(),可以在父类方法中创建子类实例。还可以用 this 类型返回当前实例的实际类型。例如:
class Base { clone(): this { return new (this.constructor as any)(); } } class Sub extends Base {} const s = new Sub(); const s2 = s.clone(); // s2 的类型是 Sub -
用途
常用于工厂方法、链式调用、流式 API 等场景。 -
灵活性
TypeScript 允许通过 this 类型实现更灵活的“new 自己”,支持继承链上的多态。
三、核心区别对比
| 维度 | Java | TypeScript |
|---|---|---|
| 语法 | 只能 new 类名()
|
可以 new 类名(),也可以 new (this.constructor as any)()
|
| 类型 | new 的总是当前类类型 | 支持 this 类型,多态更强 |
| 继承下行为 | 子类 new 父类名,得到父类对象 | 子类可通过 this.constructor 得到子类对象 |
| 运行时机制 | 编译期类型固定,运行时无多态 new | 运行时可动态 new 当前实例的构造函数 |
| 典型应用 | 工厂方法、原型模式 | 工厂方法、链式调用、流式 API |
四、TS 中 constructor 的访问修饰符
TypeScript 中,constructor 默认是 public,所以在类的外部和内部都可以通过 new 关键字来实例化对象。
class Person {
constructor() {
console.log("Person 构造函数被调用");
}
}
const p = new Person(); // 合法
如果你将 constructor 显式声明为 private 或 protected,则只能在类的内部(或子类内部,protected 情况下)通过 new 创建实例,外部无法 new。例如:
class Person {
private constructor() {}
static getInstance() {
return new Person();
}
}
const p = new Person(); // 报错:构造函数为私有
const p2 = Person.getInstance(); // 合法
protected 构造函数允许子类继承和实例化,但禁止外部直接 new 父类:
class Base {
protected constructor() {}
}
class Sub extends Base {
constructor() {
super();
}
}
const b = new Base(); // 报错
const s = new Sub(); // 合法
五、Java 中的构造器访问控制
Java 也有类似的访问修饰符(public、protected、private),但和 TypeScript 有细微差别:
- public:任何地方都能 new
- protected:同包或子类能 new
- private:只能在类内部 new
例如:
public class Singleton {
private Singleton() {}
public static Singleton getInstance() {
return new Singleton();
}
}
和 TypeScript 的单例写法如出一辙。
六、常见面试题与陷阱
1. Java 子类能否在父类方法中 new this?
不能。Java 的 this 代表当前实例,不能直接用 new this()。如果想在父类方法中创建“当前实际类型”的对象,通常需要借助反射:
public class Base {
public Base create() throws Exception {
return this.getClass().getDeclaredConstructor().newInstance();
}
}
但这种写法有一定的复杂性和性能开销。
2. TypeScript 的 this 类型陷阱
虽然 TS 支持 this 类型,但如果构造函数有参数,this.constructor as any 的类型检查就会失效,容易出错:
class A {
constructor(public name: string) {}
clone(): this {
// 这里需要传递参数,否则报错
return new (this.constructor as any)(this.name);
}
}
七、总结与最佳实践
- Java:类内 new 自身只能 new 明确的类名,继承时不会多态。需要多态时可用反射或工厂模式。
- TypeScript:类内 new 自身可以用 this.constructor 实现多态,配合 this 类型更灵活。constructor 默认 public,访问控制可实现单例等模式。
建议:
- 需要多态工厂时,TypeScript 推荐用 this 类型+静态工厂方法。
- Java 推荐用工厂方法或反射,避免直接在父类中 new 子类。
Langchain4j Rag 知识库教程
Langchain4j Rag 知识库教程
Rag 原理
RAG,Retrieval Augmented Generation,检索增强生成。通过检索外部知识库的方式增强大模型的生成能力。
基础大模型训练完成后,随着时间的推移,产生的新数据大模型是无法感知的;而且训练大模型的都是通用数据,有关专业领域的数据大模型也是不知道的。此时就需要外挂一个知识库。
其中,2.3 组织Prompt、3.1 发送Prompt、3.2 生成结果、3.3 返回响应、4 返回响应的流程由 Langchain4j 来完成。
向量知识库
向量数据库: Milvus、Chroma、Pinecone、RedisSearch(Redis)、pgvector(PostgreSQL) 向量是表示具有大小和方向的量。
向量余弦相似度,用于表示坐标系中两个点之间的距离远近
多维向量余弦相似度
向量知识库索引和检索
索引(存储)
向量存储步骤:
- 把最新或者专业的数据存储到文档(Document)中
- 文本分割器把一个大的文档切割成一个一个小的文本片段(Segments)
- 这些小的文本片段需要用一种专门擅长文本向量化的向量大模型转换成向量(Embeddings)
- 把文本片段对应的向量存储到向量数据库(Embedding Store)中
检索
检索阶段通常在线进行,当用户提交一个应该使用索引文档回答的问题时。
这个过程可能因使用的信息检索方法而异。 对于向量搜索,这通常涉及嵌入用户的查询(问题) 并在嵌入存储中执行相似度搜索。
然后将相关片段(原始文档的片段)注入到提示中并发送给 LLM。
如果余弦相似度 > 0.5的数据会被检索出来,然后再把检索结果和用户输入发送给大模型,大模型响应后返回给用户。
Rag 快速入门
存储:构建向量数据库操作对象
引入依赖
<!-- 提供向量数据库和向量模型 -->
<dependency>
<groupld>dev.langchain4j</groupld>
<artifactld>langchain4j-easy-rag</artifactld>
<version>1.0.1-beta6</version>
</dependency>
加载知识数据文档
List<Document> documents = ClassPathDocumentLoader.loadDocuments("文档路径");
构建向量数据库操作对象
InMemoryEmbeddingStore<TextSegment> store = new InMemoryEmbeddingStore<>();
把文档切割、向量化并存储到向量数据库中
EmbeddingStorelngestor ingestor = EmbeddingStorelngestor.builder()
.embeddingStore(store)
.build();
ingestor.ingest(documents);
检索:构建向量数据库检索对象
构建向量数据库检索对象
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store) // 指定向量数据库
.maxResults(3) // 最高、最多检索结果的数量
.minScore(0.6) // 最小余弦相似度
.build();
配置向量数据库检索对象
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,
contentRetriever = "retriever"
)
Rag 核心 API
Document Loader 文档加载器
用于把磁盘或者网络中的数据加载进程序,常用的文档加载器:
- FileSystemDocumentLoader,根据本地磁盘绝对路径加载
- ClassPathDocumentLoader,相对于类路径加载
- UrlDocumentLoader,根据url路径加载
Document Parser 文档解析器
用于解析使用文档加载器加载进内存的内容,把非纯文本数据转化成纯文本,常用的文档解析器:
- TextDocumentParser,解析纯文本格式的文件
- ApachePdfBoxDocumentParser,解析pdf格式文件
- ApachePoiDocumentParser,解析微软的office文件,例如DoC、PPT、XLS
- ApacheTikaDocumentParser(默认),几乎可以解析所有格式的文件
Document Splitter 文档分割器
用于把一个大的文档,切割成一个一个的小片段,常用的文档分割器:
- DocumentByParagraphSplitter,按照段落分割文本
- DocumentByLineSplitter,按照行分割文本
- DocumentBySentenceSplitter,按照句子分割文本
- DocumentByWordSplitter,按照词分割文本
- DocumentByCharacterSplitter,按照固定数量的字符分割文本
- DocumentByRegexSplitter,按照正则表达式分割文本
- DocumentSplitters.recursive(...)(默认),递归分割器,优先段落分割, 再按照行分割,再按照句子分割,再按照词分割
Embedding Model 向量模型
用于把文档分割后的片段向量化或者查询时把用户输入的内容向量化
Langchain4j 内置的向量模型
内置的向量模型可能不是那么强大,需要在
application.yml中配置第三方更强大的向量模型
配置完成后 Langchain4j 会根据配置信息向容器中注入一个向量模型对象,我们只需要把该向量模型对象设置给
EmbeddingStoreIngestor和EmbeddingStoreContentRetriever即可。
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)
.documentSplitter(ds)
.embeddingModel(embeddingModel)
.build();
ContentRetriever contentRetriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.minScore(0.5)
.maxResults(3)
.build();
EmbeddingStore 向量数据库操作对象
配置 RedisSearch 向量数据库
参考链接:
js中如何隐藏eval关键字?
本文介绍了JavaScript中隐藏eval关键字的多种方法,从简单的字符串拼接和Function构造函数,到使用字符编码动态生成字符串。更复杂的方案包括通过JS混淆工具(如JShaman)将代码转换为难以辨识的格式,甚至模拟虚拟机执行字节码来重构eval。这些技术通过层层包装,使原始eval调用在代码审计中难以被发现。
JavaScript中隐藏eval关键字的技巧
某些情况下,我们在进行JS编程时,可能想要用eval执行一些特殊的代码,但想不想让他人轻易看出是使用了eval。那么,就得想办法隐藏eval关键字了。
简单的隐藏方法
// 方法1:使用字符串分割
const ev = "ev";
const al = "al";
const hiddenEval = window[ev + al];
// 使用
hiddenEval('console.log("隐藏的eval执行")');
// 方法2:通过Function构造函数
const executeCode = new Function('code', 'return eval(code)');
executeCode('2 + 2'); // 返回4
复杂的隐藏方法
// 使用字符编码
const encodedEval = () => {
const chars = [101, 118, 97, 108];
const str = String.fromCharCode(...chars);
return window[str];
};
const myEval = encodedEval();
更更更复杂的隐藏方法
如果还想隐藏的更深,可以再用JShaman进行JS代码混淆加密,上面代码会变成:
const encodedEval = () => {
const _0x35ea38 = {
"\u006d\u004f\u0067\u006c\u0048": function (_0x55d02e, _0x5cdb2b) {
return _0x55d02e ^ _0x5cdb2b;
},
"\u0076\u006a\u0048\u0044\u0073": function (_0x4c98c3, _0xa2b4f0) {
return _0x4c98c3 ^ _0xa2b4f0;
}
};
const _0x2cd5ff = [0x47a4d ^ 0x47a28, _0x35ea38["\u006d\u004f\u0067\u006c\u0048"](0xd8290, 0xd82e6), _0x35ea38['vjHDs'](0xb9759, 0xb9738), _0x35ea38["\u0076\u006a\u0048\u0044\u0073"](0x7b450, 0x7b43c)];
const _0x3d45d7 = String['fromCharCode'](..._0x2cd5ff);
return window[_0x3d45d7];
};
const myEval = encodedEval();
或:
function _0x927a(opcode) {
var op = {
push: 32,
add: 33,
sub: 34,
mul: 35,
div: 36,
pop: 37,
xor: 38
};
var stack = [];
var ip = -1;
var sp = -1;
while (ip < opcode.length) {
ip++;
switch (opcode[ip]) {
case op.push:
{
ip++;
stack.push(opcode[ip]);
sp++;
break;
}
case op.add:
{
stack.push(stack[sp - 1] + stack[sp]);
sp++;
break;
}
case op.sub:
{
stack.push(stack[sp - 1] - stack[sp]);
sp++;
break;
}
case op.mul:
{
stack.push(stack[sp - 1] * stack[sp]);
sp++;
break;
}
case op.div:
{
stack.push(stack[sp - 1] / stack[sp]);
sp++;
break;
}
case op.xor:
{
stack.push(stack[sp - 1] ^ stack[sp]);
sp++;
break;
}
case op.pop:
{
return stack[sp];
}
}
}
}
const encodedEval = () => {
const chars = [_0x927a([32, 865932, 32, 866025, 38, 37]), _0x927a([32, 625917, 32, 625803, 38, 37]), _0x927a([32, 750963, 32, 750866, 38, 37]), _0x927a([32, 753540, 32, 753640, 38, 37])];
const str = String['\x66\x72\x6f\x6d\x43\x68\x61\x72\x43\x6f\x64\x65'](...chars);
return window[str];
};
const myEval = encodedEval();
怎么样,还能找到eval关键字吗?
图吧工具箱-电脑硬件圈的“瑞士军刀”
软件定位
- 全称:图拉丁吧硬件检测工具箱(民间简称“图吧工具箱”)
- 价格:永久免费,无广告,不开会员
- 体积:185 MB 左右(Win 版),支持 Windows 7/8/10/11 64 位
- 内核:收录 CPU-Z、GPU-Z、AIDA64、HWiNFO、FurMark、Prime95、DisplayX 等官方最新版,统一图形菜单,一键调用,无需重复安装
亮点
- 全面支持 Intel 13/14 代酷睿 与 NVIDIA RTX 40/50 系显卡 检测
- 新增 DDR5 内存、PCIe 5.0 SSD 信息识别
- 显存温度压力测试回归,一键烤卡不再闪退
- 工具库持续更新:CPU-Z、GPU-Z、HWiNFO、FurMark2 全部升至最新版
- 适配 Windows 12 预览版,高分辨率界面自动缩放
功能分区
| 硬件信息 | CPU-Z / GPU-Z / HWiNFO | 处理器、主板、内存、显卡详细参数一次看清 |
| 性能测试 | FurMark、Prime95、3DMark Demo | 显卡烤机、CPU 压力、温度/功耗/频率曲线实时监控 |
| 外设检测 | DisplayX、Keyboard Test | 屏幕坏点、色域、键盘按键触发测试 |
| 硬盘工具 | CrystalDiskInfo、AS SSD | 通电时间、健康度、PCIe 速率、4K 读写跑分 |
| 综合检测 | AIDA64 工程版 | 一键生成 40 页硬件报告,买二手电脑防翻车 |
| 系统维护 | DDU 驱动卸载、Everything 搜索 | 清旧驱动、秒搜文件,重装系统必备 |
实用场景
- 买新机/二手:先跑 HWiNFO + CrystalDiskInfo,看通电时长、电池循环、显卡核心/显存温度,避免矿卡翻新
- 装系统后:DDU 清旧驱动 → FurMark 20 分钟烤卡 → Prime95 30 分钟烤 CPU,确认散热压得住
- 升级前规划:主板型号、电源瓦数、PCIe 插槽版本一目了然,防止“i7 配 B660”尴尬
图吧工具箱 = 硬件圈的“瑞士军刀” :买机、验机、烤机、清驱动、查坏点、跑分、写报告,一个安装包全搞定;免费、无广告、持续更新,电脑装机必备!
「图吧工具箱2025.07R2安装包.exe」 链接:pan.quark.cn/s/5c68cbac6…
Xrecode3(多功能音频转换工具)
Xrecode3是一款功能强大的音频转换工具,它能够将多种音频格式相互转换,并提供了许多其他的音频处理功能。
软件功能
音频格式转换:支持将大多数常见的音频格式互相转换,包括MP3、FLAC、WAV、AAC、OGG等。
批量转换:可以同时转换多个文件,节省大量时间和精力。
支持多通道的音频:可以处理多通道音频文件,并保存每个通道的独立文件。
提供了高质量的音频编码器:内置了多种高质量音频编解码器,保证转换过程中音质不损失。
支持CUE文件:能够直接读取CUE文件,并根据CUE文件分割音轨。
ID3标签编辑:可以编辑音频文件的ID3标签,包括歌曲标题、艺术家、专辑等信息。
内置音频播放器:可以通过内置音频播放器来预览转换后的音频文件。
提供了音频剪辑功能:可以从音频文件中剪切出所需部分。
支持多核处理器:利用多核处理器的优势,加快转换速度。
软件特点
界面简洁直观:操作简单易懂,即使对音频处理不熟悉的人也能够轻松上手。
多种输出配置:提供了许多输出配置选项,用户可以根据需要自定义输出文件的音频格式、采样率、码率等参数。
高度可定制化:支持设置转换任务的优先级,可以根据需要调整转换的顺序。
快速转换速度:利用了硬件加速和多线程处理技术,大大提高了转换速度。
资源占用少:在转换过程中占用的系统资源较少,运行稳定、流畅。
「Xrecode3(多功能音频转换工具) 」 链接:pan.quark.cn/s/1480b6f1d…
Subtitle Edit(字幕编辑软件) 中文绿色版
Subtitle Edit 是一款功能强大的免费字幕编辑软件,它支持多种字幕格式,包括 SRT、SSA、ASS、SUB、LRC、TXT 等常用格式,可以实现快速创建、编辑和同步字幕文件。
软件功能
- 支持多种字幕格式:SRT、SSA、ASS、SUB、LRC、TXT 等。
- 可以实时预览字幕效果,方便编辑和调整字幕。
- 支持批量处理字幕文件,快速完成字幕制作任务。
- 支持语音识别功能,可以将视频的音频转换成文本字幕。
- 支持多种翻译工具,可以进行实时翻译和字幕翻译。
- 支持字幕同步功能,可以根据视频的时间轴自动调整字幕时间点。
- 支持多种字体和样式选择,可以自定义字幕风格。
软件特点
- 界面简洁、操作简单,易于上手。
- 支持多种语言界面,包括中文、英文、日文等。
- 提供丰富的字幕编辑功能,包括剪切、复制、粘贴、删除、移动等。
- 支持多种视频格式,包括 AVI、MP4、MKV、WMV 等。
- 支持多种字幕语言,包括中文、英文、法文、西班牙文等。
- 提供多种字幕效果,包括字体大小、颜色、样式、阴影、描边等。
- 支持字幕翻译功能,可以根据用户需求自动翻译字幕。
总之,Subtitle Edit 是一款功能强大、易于操作的字幕编辑软件,可以帮助用户快速创建、编辑和同步字幕文件,是制作字幕的不错选择。
中文设置
Options – Choose Language… – 中文简体 – OK。
「Subtitle Edit(字幕编辑软件) v4.0.14 中文绿色版」 链接:pan.quark.cn/s/7c01467e1…