Node.js SyntaxError: Unexpected identifier:非 ASCII 字符混入代码的排查
中文字符导致 SyntaxError SyntaxError: Unexpected identifier '目' at wrapSafe (node:internal/modules/cjs/loader:...)报错指向中文字符"目",不是代码写法问题,而是文本内容被误粘贴进了 JS 文件: // 错误:文本直接出现在代码里 const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length);题 目 AI辅助的全方位科研管理与创作平台 // ← 这行是数据,不是代码最后一行是从 1.txt 复制过来的内容,Node 解析时把它当代码执行,"题"和"目"之间有空格被当作两个标识符。 修复方法:删除误粘贴的文本,JS 文件只保留代码逻辑。 readFileSync 的编码参数 // 不指定编码:返回 Buffer const buf = fs.readFileSync("1.txt"); console.log(buf.length); // 字节数(中文 UTF-8 每字 3 字节)// 指定 utf8:返回字符串 const text = fs.readFileSync("1.txt", "utf8"); console.log(text.length); // 字符数(JavaScript 的 UTF-16 码元数) console.log(Buffer.byteLength(text)); // UTF-8 字节数中文字符用 buf.length 统计结果是字节数(通常是字符数的 3 倍),必须指定 "utf8" 才能拿到字符串后再用 .length。 用 Map 统计数组重复项 Map 是计数/去重的高效结构: const arr = ['apple', 'banana', 'apple', 'cherry', 'banana', 'apple'];const counter = new Map(); for (const item of arr) { counter.set(item, (counter.get(item) ?? 0) + 1); } // Map { 'apple' => 3, 'banana' => 2, 'cherry' => 1 }找出出现超过 1 次的元素: const duplicates = [...counter.entries()] .filter(([, count]) => count > 1) .map(([key]) => key); // ['apple', 'banana']一遍遍历即可,时间复杂度 O(n),比嵌套 filter/indexOf 更高效。 去重(保留唯一值) 用 Set 更简洁: const unique = [...new Set(arr)]; // ['apple', 'banana', 'cherry']如果同时需要计数和去重,先建 Map 再从 Map 的 key 拿唯一值: const unique = [...counter.keys()];
LLM Tool Calling 的四种 role 和 "不用调工具" 的空数组约定
写 LLM Agent 或者接 Function Calling / Tool Use 的时候,role 字段是消息路由的核心。四种 role 分工明确,工具调用要按固定生命周期走。 四种 role 1. system — 全局规则 设定模型的行为准则、可用工具、输出格式约束: { "role": "system", "content": "你是一个只回答天气问题的助手。工具调用格式必须严格 JSON。" }优先级最高。一段对话通常只有一条 system 消息(放最前面)。 2. user — 用户输入 真实用户的问题、指令、上下文: { "role": "user", "content": "上海明天下雨吗?" }3. assistant — 模型响应 模型的回复。可以是普通文本、也可以是 tool_call 请求: 普通回复: { "role": "assistant", "content": "上海明天多云,温度 15-22°C。" }发起工具调用: { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"上海\",\"date\":\"tomorrow\"}" } } ] }content 为 null 表示模型选择用工具而不是直接回答。 4. tool — 工具执行结果 外部函数运行完,把结果塞回上下文让模型继续: { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"weather\":\"多云\",\"temp\":\"15-22\"}" }tool_call_id 必须和上一条 assistant.tool_calls[].id 对上——多工具并行时靠这个匹配。 完整生命周期 user → "上海明天天气" ↓ assistant → tool_calls: [ get_weather({city:"上海"}) ] ↓ [外部执行 get_weather,返回 "多云 15-22°C"] ↓ tool → "多云 15-22°C" (tool_call_id = call_abc123) ↓ assistant → "上海明天多云,15-22°C,建议带件外套"四轮消息、四种 role。每条 tool 消息必须对应上一轮某个 tool_call,不能凭空出现。 "不需要工具"的返回约定 有些 Agent 框架要求 assistant 明确表达"这一轮我不需要工具"。约定俗成的写法是空数组: { "role": "assistant", "content": "你好,我是助手。", "tool_calls": [] }严格规范里:不需要工具 → tool_calls: [](或者干脆不带该字段) 需要工具 → tool_calls: [{...}][] 明确表示"模型已经判断过、决定不用工具",比 null 或缺字段更清晰。写自己的 tool routing 时,这样解析更省心: if (Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0) { executeToolCalls(msg.tool_calls); } else { displayText(msg.content); }并行工具调用 现代模型(GPT-4、Claude 3.5+)支持一次返回多个 tool_calls: { "role": "assistant", "tool_calls": [ { "id": "call_1", "function": {"name": "get_weather", "arguments": "{\"city\":\"上海\"}"} }, { "id": "call_2", "function": {"name": "get_weather", "arguments": "{\"city\":\"北京\"}"} } ] }Agent 应该并发执行两个 get_weather,然后按顺序 append 两条 tool 消息回上下文: [ { "role": "tool", "tool_call_id": "call_1", "content": "上海:多云" }, { "role": "tool", "tool_call_id": "call_2", "content": "北京:晴" } ]再让模型继续。 常见坑 1. tool_call_id 忘了对齐 { "role": "assistant", "tool_calls": [{"id": "call_1", ...}] }, { "role": "tool", "tool_call_id": "call_2", ... } // 对不上模型下一轮会困惑——大概率报错或者胡说。每个 tool_call 必须对应恰好一个 tool 消息。 2. 直接输出工具调用当文本 有些开发者 prompt 里让模型 "输出 <tool>...</tool> 格式",然后自己解析。能用原生 tool_calls 就用,稳定性和生态好得多(错误处理、并行、streaming 都有官方支持)。 3. 工具报错没处理 工具执行失败,应该把错误信息作为 tool 消息返回,让模型知道并决定重试或换策略: { "role": "tool", "tool_call_id": "call_1", "content": "{\"error\":\"API rate limit exceeded, retry after 60s\"}" }而不是抛异常终止对话。 4. 工具太多导致 token 爆炸 每次请求都要把所有 tool schema 塞进去。只给模型看当前场景需要的工具子集——按用户意图动态选。20 个以上工具建议做工具路由。 OpenAI / Anthropic 差异字段 OpenAI Anthropic Clauderole: assistant 里的工具调用 tool_calls: [] content 里是 array,含 type: "tool_use" 项工具结果 role tool user(但 content 里是 type: "tool_result")工具定义位置 tools: [] 参数 tools: [] 参数(结构略不同)Anthropic 把工具结果放 user role 是历史原因——本质数据一样,只是包装略不同。 一句话总结 四种 role:system 全局规则、user 用户输入、assistant 模型输出(可含 tool_calls)、tool 外部执行结果。"不用工具"的规范返回是 tool_calls: []。每个 tool 消息必须靠 tool_call_id 对齐前一轮的调用。
JS 生成 UUID:crypto.randomUUID 与 uuid npm 包
crypto.randomUUID(现代标准方案) 浏览器和 Node.js 都内置支持: const id = crypto.randomUUID(); console.log(id); // "3f6c2a6f-8d7d-4f9c-b7d5-3fcb52c8a1aa"这是 UUID v4(随机),格式固定为 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx。Chrome / Edge / Firefox 均支持 Node.js ≥ 19 直接用全局 crypto;Node.js 15–18 从 crypto 模块导入// Node.js 15-18 const { randomUUID } = require('crypto'); console.log(randomUUID());uuid npm 包 适合需要 v1 / v5 或更好兼容性的场景: npm install uuidimport { v4 as uuidv4 } from 'uuid';console.log(uuidv4()); // "110e8400-e29b-41d4-a716-446655440000"各版本:版本 特点 场景v1 时间 + MAC 地址 需要可排序的 IDv4 全随机 最常用,通用唯一 IDv5 命名空间 + hash 相同输入生成相同 ID老版本兼容写法 不能用 crypto.randomUUID 时的回退: function uuid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; const v = c === 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); }Math.random() 不是密码学安全随机数,高并发下理论上存在碰撞风险,生产环境优先用 crypto.randomUUID()。 短 ID(不是 UUID) 只需要一个不太长的唯一标识,不要求标准格式时: const id = Math.random().toString(36).slice(2); // "k9x2m8q"toString(36) 用 0–9 + a–z 36进制,slice(2) 去掉开头的 0.。这个不是 UUID,不保证唯一性,只适合临时标识(如 DOM id、session key)。 更长更安全的短 ID: const id = crypto.getRandomValues(new Uint8Array(8)) .reduce((acc, b) => acc + b.toString(16).padStart(2, '0'), ''); // "a3f2c1d9e4b70821"使用场景数据库主键:用 UUID v4,全随机防枚举 请求追踪:X-Request-ID header 用 crypto.randomUUID() 前端元素 key:用短 ID 即可 内容寻址:用 UUID v5,同 URL 生成相同 ID
logTimingOnce 是啥 + 前端性能计时的正规做法
有人问 logTimingOnce 是 JS 内置什么函数——不是。这个名字不是浏览器标准 API,也不在 Node 内置里。多半是:某个项目里自己定义的工具函数 某个框架 / SDK 内部封装 被压缩混淆后的名字从名字看是 log + Timing + Once——只记录一次耗时日志。列几种可能的实现和前端性能计时的正规做法。 猜测的实现 版本 1:一次性计时器 function logTimingOnce(name, fn) { const start = performance.now(); const result = fn(); const end = performance.now(); console.log(`${name}: ${(end - start).toFixed(2)}ms`); return result; }// 用法 const data = logTimingOnce("parseJSON", () => JSON.parse(bigStr));版本 2:同一 key 只打一次 const logged = new Set();function logTimingOnce(key, cost) { if (logged.has(key)) return; logged.add(key); console.log(`[timing] ${key}: ${cost}ms`); }// 用法 const start = performance.now(); initWebGL(); logTimingOnce("webgl-init", performance.now() - start); // 只打一次大型 SPA / 游戏 / WebGL / AI 前端里这种模式常见——首屏耗时、初始化时间只统计一次。 想知道具体项目里 logTimingOnce 是哪种,直接: console.log(logTimingOnce.toString());正规姿势:performance.now Date.now() 精度只到毫秒,且系统时钟被调整还会跳(NTP 校时、用户改时间)。计时永远用 performance.now(): const start = performance.now(); // ... 做事 const cost = performance.now() - start; console.log(cost); // 精度到微秒(0.001ms)单调时钟,不会倒退,浏览器和 Node 都支持。 User Timing API:让 DevTools 看得见 上面的 console.log 输出到控制台,看着乱。用 User Timing API 把标记打到 Performance 面板: performance.mark("webgl-init-start"); initWebGL(); performance.mark("webgl-init-end");performance.measure( "webgl-init", "webgl-init-start", "webgl-init-end" );之后 Chrome DevTools → Performance 面板里能看到 webgl-init 这条竖线,跟其它内置事件(LCP、FCP)在一起。 拿测量结果: const [entry] = performance.getEntriesByName("webgl-init"); console.log(entry.duration);PerformanceObserver:订阅性能事件 要监听所有 measure(比如上报到日志系统): const observer = new PerformanceObserver(list => { for (const entry of list.getEntries()) { console.log(entry.name, entry.duration); // 上报到你的日志服务 } }); observer.observe({ entryTypes: ["measure"] });也能监听浏览器内置事件: // LCP(Largest Contentful Paint) new PerformanceObserver(list => { const last = list.getEntries().pop(); console.log("LCP:", last.startTime); }).observe({ type: "largest-contentful-paint", buffered: true });常用性能指标一览指标 什么意思 怎么拿FCP (First Contentful Paint) 首次有内容渲染 paint entryTypeLCP (Largest Contentful Paint) 最大内容渲染 largest-contentful-paintFID (First Input Delay) / INP 首次 / 最长交互延迟 event、first-inputCLS (Cumulative Layout Shift) 布局偏移 layout-shiftTTFB (Time to First Byte) 首字节时间 navigation entryType想一次性拿 Web Vitals,直接用 Google 的库: npm install web-vitalsimport { onCLS, onLCP, onINP } from "web-vitals"; onCLS(console.log); onLCP(console.log); onINP(console.log);顺带:JS 生成 UUID 原对话里还问了 UUID,顺便说下——现代浏览器和 Node 直接用: crypto.randomUUID(); // "3f6c2a6f-8d7d-4f9c-b7d5-3fcb52c8a1aa"比自己写的 Math.random 版本安全(用 CSPRNG)、代码短。老浏览器不支持时才需要 polyfill。 一句话总结 logTimingOnce 是项目自定义函数,不是标准 API。真正测性能用 performance.now() 做基础,performance.mark/measure 让 DevTools 看得见,PerformanceObserver 采集上报。UUID 直接 crypto.randomUUID()。
为什么 forEach 里的 await 没用:JS 异步循环踩坑记
前几天写脚本,想在数组遍历里挨个 await 一下: const arr = [1, 2, 3];arr.forEach(async (item) => { await sleep(1000); console.log(item); });console.log('结束');预期看到 1 2 3 结束,结果输出是: 结束 1 2 3forEach 完全没等异步回调结束,console.log('结束') 就先跑了。 为什么 forEach 不等待 看 Array.prototype.forEach 的内部实现,本质就是: for (let i = 0; i < arr.length; i++) { callback(arr[i]); }它 不关心 callback 返回的是不是 Promise,也不收集 Promise,更不会 await。所以哪怕你写: await arr.forEach(async x => await fn(x));等价于: await undefined因为 forEach 的返回值就是 undefined。 正确写法 需要顺序 await:用 for...of 一个执行完再执行下一个,最稳、最好读: for (const item of arr) { await doSomething(item); }适合请求限流、数据库写入、有依赖关系的自动化脚本。 需要并发 await:用 Promise.all + map 全部同时开跑,最快: await Promise.all( arr.map(async (item) => { await doSomething(item); }) );注意别把上游接口打爆——需要限流的话可以配合 p-limit 这类库。 记忆表场景 写法顺序 await for...of并发 await Promise.all + map不需要等待 forEach 也无所谓一条经验 只要方法里出现 await,就别用 forEach。 想清楚这一步之后,很多"数据处理完了但结果不对"的 bug 就会消失。
