Agent开发的核心不在模型,而在模型之外的Harness工程层
2026/9/16 18:05:08 网站建设 项目流程

1. 这句话到底在说啥:Agent 的差距不在模型,在模型之外这一层

“Agent 的差距不在模型,在模型之外这一层”——这句话最近在技术圈刷屏,不是因为多新奇,而是因为它戳中了当前 AI 应用落地最真实的痛点。我带过 7 个从零启动的 Agent 项目,覆盖客服调度、金融风控辅助、工业设备巡检、教育内容生成、电商智能导购、医疗问诊预筛、政务知识助手这七个完全不同的垂直场景,每个项目都经历过“换模型→效果没变→再换→还是卡在那儿”的循环。最后发现,真正卡住进度、拖垮交付周期、让客户反复质疑“AI 到底有没有用”的,从来不是 LLM 换成了 Qwen3 还是 DeepSeek-V3,而是模型调用链路上那一层看不见、摸不着、但处处要命的“模型之外”。

这一层,不是 Prompt 工程师写的那几行 system message,也不是 RAG 检索回来的 chunk 拼接逻辑,更不是某个 fancy 的 planner 架构图。它是真实跑在生产环境里的工程实体:是 JavaScript 里一个fetch调用超时后要不要重试、重试几次、退避间隔怎么算;是用户连续三次输入模糊指令后,系统该触发 fallback 流程还是静默降级;是当大模型返回 JSON 格式错了一位逗号,前端解析直接崩溃,还是能自动 trim + 容错修复;是本地缓存策略该用 Map 还是 IndexedDB,缓存 key 是按 session ID 还是按 query hash;是错误日志里那句 “TypeError: Cannot read property 'choices' of undefined” 背后,到底是模型 API 返回了空响应,还是网络中间件把 status 200 当成成功、却忽略了 body 为空的语义。

它叫Harness——不是某个具体开源库的名字,而是一种工程范式。DeepSeek Harness、Codex Harness、TestPilot Harness……这些名字里的 “Harness”,本质都是对“模型调用”这件事的标准化封装:统一输入/输出契约、内置重试与熔断、结构化错误分类、可插拔的预处理/后处理钩子、可观测性埋点、资源配额控制。就像汽车的底盘和悬挂系统——发动机(模型)再强,没有可靠的底盘(Harness),车开上路就是颠簸、失控、爆胎。我们团队内部管它叫“模型驾驶舱”,因为真正的驾驶体验,90% 取决于方向盘、油门响应、ABS 和 ESP,而不是引擎排量。

所以,如果你正在做 Agent 开发,或者正被老板催着“快上线个 AI 功能”,请先放下手头那个还在调 temperature=0.7 还是 0.3 的 notebook。先问自己三个问题:第一,当模型 API 响应延迟从 800ms 突然涨到 4s,你的前端是白屏 4 秒,还是立刻展示“正在深度思考中…”并启动本地缓存兜底?第二,当用户输入“帮我查下上个月张三的报销单”,模型返回了一段自然语言描述,你的系统是原样吐给用户,还是能自动识别出“张三”“报销单”“上个月”这三个关键槽位,并触发后续数据库查询?第三,当某次调用返回了格式错误的 JSON,你的代码是直接 throw new Error 把整个流程崩掉,还是捕获后打日志、记录失败样本、并返回一条友好的提示语?

这三个问题的答案,决定了你做的到底是一个能跑通 demo 的玩具,还是一个能在真实业务里扛住每天 5 万次请求、99.95% 可用率、用户愿意持续使用的 Agent。而它们,全部落在“模型之外这一层”。

2. 拆解“模型之外这一层”:Harness 的四大核心模块与 JavaScript 实现逻辑

很多人误以为 Harness 就是个“API 调用封装函数”,写个async function callLLM(prompt)就完事。实测下来,这种写法在 Demo 阶段很丝滑,一进压测就露馅。真正的 Harness 是一套有状态、可配置、可观测、可演进的工程系统。我们以 JavaScript(Node.js + Express / Next.js / Deno)为典型环境,拆解其必须包含的四大核心模块,每个模块都对应真实踩过的坑和验证过的方案。

2.1 输入适配与上下文编织模块(The Context Weaver)

模型不理解“用户”是谁,“对话历史”意味着什么,“当前页面状态”如何影响回答。Harness 必须把散落各处的信息,编织成模型能消化的 context。这不是简单拼字符串。

  • 动态上下文窗口管理:不能无脑塞满 32k token。我们采用“分层加权”策略:最近 3 轮对话(权重 1.0)、用户 profile(权重 0.8)、当前页面 URL 和 DOM 关键字段(权重 0.6)、全局业务规则(权重 0.4)。JavaScript 实现时,用Array.reduce()计算每段文本的 token 估算值(基于gpt-tokenizer@dqbd/tiktoken),动态截断低权重部分,确保总长度严格 ≤ 模型最大上下文 - 512(预留输出空间)。实测比固定截断提升 22% 的指令遵循率。

  • 结构化指令注入:避免在 prompt 里写“请用 JSON 格式返回”。Harness 在发送前,自动注入 system message 片段:

    const systemPrompt = ` You are a ${role} assistant. Output MUST be valid JSON with exactly these keys: ${JSON.stringify(requiredKeys)}. If any field is unknown, use null. NEVER add extra fields or markdown. `;

    并配套后处理校验——这是关键。很多团队只做前半截,结果模型偶尔还是返回带json包裹的字符串,导致 JSON.parse 失败。

  • 敏感信息脱敏钩子:用户输入“我的身份证是11010119900307231X”,Harness 在送入模型前,必须自动替换为[ID_CARD],并在模型返回后,用映射表还原。JavaScript 里我们用replace()配合正则 + Map 缓存,避免重复编译正则。这个钩子必须可开关、可审计——某次上线后发现脱敏漏掉了邮箱后缀,靠日志回溯才定位。

提示:不要在 prompt 里写“请不要泄露用户隐私”。Harness 的职责是物理隔离,不是道德约束。模型看到的永远是脱敏后的数据。

2.2 输出解析与结构化转换模块(The Parser Engine)

模型输出是“文本”,业务需要的是“数据”。这一层的健壮性,直接决定下游能否自动化。我们见过太多项目,因为模型返回了“好的,已为您查询到:张三,报销单号 BX202405001,金额 ¥3,200.00”,就直接当 JSON 用,结果线上报错Cannot read property 'amount' of undefined

  • 多级容错解析流水线

    1. 基础清洗:去除首尾空白、BOM 字符、多余换行;
    2. Markdown 块剥离:用正则/```(?:json)?\n([\s\S]*?)\n```/提取 code block 内容;
    3. JSON 修复:调用jsonrepair库(轻量,无依赖),它能自动补全缺失的引号、括号,修正 trailing comma;
    4. Schema 校验:用zod定义输出 schema,zod.parse()强制类型检查。失败时,不是 throw,而是记录parsing_error事件,并返回默认值或触发 fallback。
  • 非 JSON 场景的协议约定:不是所有 Agent 都要 JSON。比如画图 Agent,模型返回 “A photorealistic cat wearing sunglasses, sitting on a skateboard” —— Harness 必须能识别这是 text-to-image 指令,并提取关键词、过滤敏感词、添加品牌水印参数。我们用string.split(',').map(s => s.trim())做初步切分,再用预定义关键词库匹配意图。

  • 流式响应的增量解析:SSE 场景下,模型边想边说。Harness 必须能 buffer partial chunks,只在收到完整 JSON object 或明确结束标记(如\n\n)后才触发解析。Node.js 的ReadableStream+TextDecoderStream是标准解法,但要注意decoder.decode(chunk, { stream: true })的流式解码,避免 UTF-8 多字节字符被截断。

2.3 执行协调与工具调用模块(The Orchestrator)

Agent 不是纯聊天机器人,它要调用天气 API、查数据库、发邮件、控制 IoT 设备。Harness 是调度中心,不是传话筒。

  • 工具描述标准化:每个工具(function calling)必须有 machine-readable 的 OpenAPI-like 描述:

    { "name": "get_weather", "description": "Get current weather for a city", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "City name in Chinese"} }, "required": ["city"] } }

    Harness 在调用前,用zod校验模型返回的tool_calls参数是否符合 schema。不符合?直接拒绝,不转发。

  • 并发与依赖管理:多个工具调用可能有依赖(如先查订单号,再用订单号查物流)。Harness 维护一个 DAG(有向无环图),用topological-sort库排序执行顺序。JavaScript 中,我们用Promise.allSettled()控制并发度(默认 3),失败节点自动跳过后续依赖节点。

  • 超时与熔断:每个工具调用单独设 timeout(如天气 API 3s,数据库查询 800ms)。超过阈值,Harness 主动 cancel fetch request(AbortController),并记录tool_timeout事件。连续 5 次失败,触发熔断,后续 60 秒内直接返回 cached response 或 fallback。

2.4 可观测性与反馈闭环模块(The Feedback Loop)

没有监控的 Harness 是盲人开车。我们要求每个核心模块必须埋点,且日志能关联同一请求的全链路。

  • 结构化日志 Schema

    { "request_id": "req_abc123", "stage": "input_adaptation | model_call | output_parsing | tool_execution", "duration_ms": 124.5, "status": "success | failed | fallback", "error_code": "parse_invalid_json | tool_timeout | model_rate_limit", "model_used": "deepseek-v3", "input_tokens": 1240, "output_tokens": 320 }

    JavaScript 中,用pino日志库,pino.child({ request_id })创建子 logger,确保所有日志带 request_id。

  • 实时指标看板:我们用 Prometheus + Grafana,暴露关键指标:

    • agent_harness_request_total{stage="model_call",status="success"}
    • agent_harness_duration_seconds_bucket{le="1.0"}
    • agent_harness_fallback_rate
      这些指标直接驱动 SLO(Service Level Objective)评估。比如model_call的 P95 延迟 > 2s,自动告警。
  • 用户反馈钩子:在 UI 层加一个“这个回答有帮助吗?”按钮。点击“无帮助”,Harness 自动捕获当前 request_id、原始输入、模型原始输出、最终返回给用户的结构化数据,存入 feedback DB。每周用这些样本 retrain parser 规则或 fine-tune prompt 模板。

这四大模块,任何一个缺失,都会让 Agent 在真实场景中变得脆弱。而它们全部运行在 JavaScript 运行时里——不是模型推理层,而是你写的每一行fetch()JSON.parse()Promise.all()背后。

3. 实操:用 JavaScript 从零搭建一个生产级 Harness(含完整代码片段)

光讲理论没用。下面是我团队在电商客服 Agent 项目中实际落地的 Harness 核心骨架,已脱敏,可直接复用。它不是一个 npm 包,而是一套可演进的工程模式。我们用 TypeScript + Node.js 18(支持 top-level await),部署在 Cloudflare Workers(边缘计算)和 AWS Lambda(高负载)双环境。

3.1 初始化与配置中心(harness/config.ts)

Harness 的灵魂是可配置。硬编码 timeout 或 retry 次数是灾难源头。

// harness/config.ts export interface HarnessConfig { // 模型调用配置 model: { endpoint: string; // 如 https://api.deepseek.com/v1/chat/completions apiKey: string; timeoutMs: number; // 全局超时 maxRetries: number; // 重试次数 }; // 上下文编织配置 context: { maxTokens: number; // 目标模型最大上下文 weights: { recentMessages: number; userProfile: number; pageState: number; businessRules: number; }; }; // 解析配置 parsing: { jsonRepair: boolean; // 是否启用 jsonrepair schema: ZodSchema; // zod schema fallbackValue: any; // 解析失败时的默认值 }; // 工具调用配置 tools: { concurrency: number; // 最大并发数 timeouts: Record<string, number>; // 各工具超时,单位 ms }; } // 生产环境从环境变量加载 export const config: HarnessConfig = { model: { endpoint: process.env.MODEL_ENDPOINT!, apiKey: process.env.MODEL_API_KEY!, timeoutMs: parseInt(process.env.MODEL_TIMEOUT_MS || '8000'), maxRetries: parseInt(process.env.MODEL_MAX_RETRIES || '2'), }, context: { maxTokens: 32768, weights: { recentMessages: 1.0, userProfile: 0.8, pageState: 0.6, businessRules: 0.4, } }, parsing: { jsonRepair: true, schema: z.object({ intent: z.enum(['order_inquiry', 'return_request', 'product_recommend']), entities: z.array(z.object({ type: z.string(), value: z.string() })), response: z.string() }), fallbackValue: { intent: 'unknown', entities: [], response: '抱歉,我没理解您的意思。' } }, tools: { concurrency: 3, timeouts: { 'get_order_status': 1200, 'initiate_return': 2500, 'search_products': 800 } } };

注意:process.env加载必须在应用启动时完成,不能在每次请求中动态读取,否则冷启动延迟飙升。Cloudflare Workers 用envbinding,Lambda 用process.env

3.2 输入适配器实现(harness/inputAdapter.ts)

核心是adaptContext函数,它把原始用户输入、会话历史、页面状态,变成模型能吃的 prompt。

// harness/inputAdapter.ts import { config } from './config'; import { estimateTokens } from './tokenUtils'; import { UserProfile } from '../types'; interface InputContext { userId: string; messages: Array<{ role: 'user' | 'assistant'; content: string }>; currentPageUrl: string; domSnapshot: string; // 关键 DOM 字段的 JSON 序列化 userProfile: UserProfile; } export class InputAdapter { adaptContext(input: InputContext): string { // 步骤1:构建分层上下文 const sections: Array<{ content: string; weight: number; tokens: number }> = []; // 最近消息(高权重) const recentMsgs = input.messages.slice(-3); const recentContent = recentMsgs.map(m => `${m.role}: ${m.content}`).join('\n'); sections.push({ content: recentContent, weight: config.context.weights.recentMessages, tokens: estimateTokens(recentContent) }); // 用户画像(中高权重) const profileContent = JSON.stringify(input.userProfile); sections.push({ content: profileContent, weight: config.context.weights.userProfile, tokens: estimateTokens(profileContent) }); // 页面状态(中权重) const pageContent = `Current URL: ${input.currentPageUrl}\nDOM Snapshot: ${input.domSnapshot}`; sections.push({ content: pageContent, weight: config.context.weights.pageState, tokens: estimateTokens(pageContent) }); // 业务规则(低权重) const rulesContent = "You are an e-commerce customer service agent. Always prioritize order safety. Never disclose user's full phone number."; sections.push({ content: rulesContent, weight: config.context.weights.businessRules, tokens: estimateTokens(rulesContent) }); // 步骤2:按权重排序,贪婪填充 sections.sort((a, b) => b.weight - a.weight); let totalTokens = 0; let finalContext = ''; for (const section of sections) { if (totalTokens + section.tokens <= config.context.maxTokens - 512) { finalContext += `\n---\n${section.content}\n---\n`; totalTokens += section.tokens; } else { // 截断当前 section,保留前 N 个 token const truncated = this.truncateByTokens(section.content, config.context.maxTokens - totalTokens - 512); finalContext += `\n---\n${truncated}\n---\n`; break; } } // 步骤3:注入结构化指令 const systemPrompt = ` You are a helpful e-commerce customer service agent. Output MUST be valid JSON with exactly these keys: ${JSON.stringify(['intent', 'entities', 'response'])}. If any field is unknown, use null. NEVER add extra fields. `; return `${systemPrompt}\n\n${finalContext}\n\nUser: ${input.messages[input.messages.length - 1].content}`; } private truncateByTokens(text: string, maxTokens: number): string { // 简单按字符截断(生产用 tiktoken 更准) const charsPerToken = 4; // 粗略估算 const maxChars = maxTokens * charsPerToken; return text.length > maxChars ? text.substring(0, maxChars) + '...' : text; } }

这个adaptContext函数,就是“模型之外”最核心的第一次加工。它决定了模型看到的世界是什么样子。

3.3 模型调用与重试器(harness/modelCaller.ts)

这才是真正的“Harness”心脏。它封装了所有网络细节。

// harness/modelCaller.ts import { config } from './config'; import { InputAdapter } from './inputAdapter'; import { parseOutput } from './outputParser'; export class ModelCaller { private readonly abortController = new AbortController(); async call( context: string, signal?: AbortSignal ): Promise<{ rawResponse: string; parsed: any }> { const startTime = Date.now(); // 重试逻辑 for (let attempt = 0; attempt <= config.model.maxRetries; attempt++) { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.model.timeoutMs); const response = await fetch(config.model.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.model.apiKey}` }, body: JSON.stringify({ model: 'deepseek-chat', messages: [{ role: 'user', content: context }], temperature: 0.3, max_tokens: 1024 }), signal: controller.signal }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const data = await response.json(); const rawResponse = data.choices?.[0]?.message?.content || ''; // 记录成功日志 console.log({ stage: 'model_call', status: 'success', duration_ms: Date.now() - startTime, input_tokens: data.usage?.prompt_tokens || 0, output_tokens: data.usage?.completion_tokens || 0 }); return { rawResponse, parsed: await parseOutput(rawResponse) }; } catch (error) { const duration = Date.now() - startTime; console.error({ stage: 'model_call', status: 'failed', duration_ms: duration, error: error instanceof Error ? error.message : String(error), attempt }); // 最后一次尝试失败,抛出 if (attempt === config.model.maxRetries) { throw error; } // 指数退避:100ms, 300ms, 900ms const backoffMs = Math.pow(3, attempt) * 100; await new Promise(resolve => setTimeout(resolve, backoffMs)); } } throw new Error('Unreachable'); } }

注意这里的AbortControllersetTimeout组合,是 JavaScript 中最可靠的手动超时方案。fetchsignal选项是标准,但必须配合setTimeout才能保证绝对超时。

3.4 输出解析器(harness/outputParser.ts)

健壮性全在这里。

// harness/outputParser.ts import { config } from './config'; import { z } from 'zod'; import { repairJson } from 'jsonrepair'; export async function parseOutput(raw: string): Promise<any> { try { // 步骤1:基础清洗 let cleaned = raw.trim(); if (!cleaned) throw new Error('Empty response'); // 步骤2:提取 JSON code block const codeBlockMatch = cleaned.match(/```(?:json)?\n([\s\S]*?)\n```/); if (codeBlockMatch) { cleaned = codeBlockMatch[1].trim(); } // 步骤3:JSON repair(如果启用) let parsed; if (config.parsing.jsonRepair) { try { const repaired = repairJson(cleaned); parsed = JSON.parse(repaired); } catch (e) { // repair 失败,fallback 到原始 parse parsed = JSON.parse(cleaned); } } else { parsed = JSON.parse(cleaned); } // 步骤4:Schema 校验 return config.parsing.schema.parse(parsed); } catch (error) { console.error({ stage: 'output_parsing', status: 'failed', error: error instanceof Error ? error.message : String(error), raw_response: raw.substring(0, 200) + '...' }); // 返回 fallback 值,不 throw return config.parsing.fallbackValue; } }

关键点:parseOutput永远不 throw。它要么返回合法数据,要么返回 fallback。上游业务逻辑不需要 try/catch,直接用。

3.5 工具调用协调器(harness/toolOrchestrator.ts)

让 Agent 真正“行动”起来。

// harness/toolOrchestrator.ts import { config } from './config'; import { ToolDefinition, ToolCall } from '../types'; export class ToolOrchestrator { async executeToolCalls( toolCalls: ToolCall[], signal?: AbortSignal ): Promise<Record<string, any>> { // 构建 DAG:分析依赖关系(这里简化,假设无依赖) const executionPlan = toolCalls.map(call => ({ name: call.function.name, args: call.function.arguments, timeoutMs: config.tools.timeouts[call.function.name] || 1000 })); // 并发执行 const results = await Promise.allSettled( executionPlan.map(async (task) => { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), task.timeoutMs); try { const result = await this.callTool(task.name, task.args, controller.signal); clearTimeout(timeoutId); return { [task.name]: result }; } catch (error) { clearTimeout(timeoutId); console.error({ stage: 'tool_execution', status: 'failed', tool: task.name, error: error instanceof Error ? error.message : String(error) }); return { [task.name]: null }; // 失败返回 null,不中断整体 } }) ); // 合并结果 const merged: Record<string, any> = {}; results.forEach(r => { if (r.status === 'fulfilled') { Object.assign(merged, r.value); } }); return merged; } private async callTool(name: string, args: any, signal: AbortSignal): Promise<any> { switch (name) { case 'get_order_status': return this.getOrderStatus(args.orderId, signal); case 'initiate_return': return this.initiateReturn(args.orderId, args.reason, signal); case 'search_products': return this.searchProducts(args.keywords, signal); default: throw new Error(`Unknown tool: ${name}`); } } private getOrderStatus(orderId: string, signal: AbortSignal): Promise<any> { return fetch(`/api/orders/${orderId}`, { signal }) .then(r => r.json()); } // 其他工具方法... }

这个executeToolCalls是 Agent 的“手脚”。它让模型的决策,变成真实的业务动作。

4. 常见问题与排查技巧实录:那些让工程师凌晨三点还在改的 Bug

再完美的设计,也逃不过现实世界的毒打。以下是我们在 7 个项目中,高频出现、反复踩坑、最终沉淀为 SOP 的 12 个典型问题。每个都附带真实日志、根因分析和一行修复代码。

4.1 问题速查表:高频故障与定位路径

故障现象日志特征根本原因修复方案修复代码行
Agent 响应突然变慢,P95 延迟从 1.2s 涨到 5.8smodel_call duration_ms: 5820status: success模型 API 未限流,单个请求占满连接池,后续请求排队在 fetch 前加连接池限制const agent = new https.Agent({ maxSockets: 10 });
用户连续提问,Agent 开始胡说八道input_tokens: 32100,context: ...(超长)上下文编织未做 token 估算,实际超出模型上限,模型截断后语义混乱tiktoken精确计算,动态截断const encoder = getEncoding("cl100k_base"); const tokens = encoder.encode(text).length;
模型返回{ "intent": "order_inquiry", "entities": [...] },但业务层parsed.intent是 undefinedparsing status: success,parsed: { intent: "order_inquiry", ... }TypeScript 类型声明与 zod schema 不一致,runtime 无问题,build 时报错统一用 zod 生成 TS 类型const MySchema = z.object({...}); type MyType = z.infer<typeof MySchema>;
工具调用失败后,Agent 直接返回“抱歉,无法处理”tool_execution status: failed,tool: get_order_status工具调用未设置 fallback,Promise.allSettled 后未处理 rejected 结果executeToolCalls中,对 rejected 结果返回默认值if (r.status === 'rejected') { return { [task.name]: { status: 'error', message: r.reason } }; }
用户说“查张三的报销单”,模型返回{"intent":"query","entity":"张三"},但数据库查不到tool_call: { name: "query_expense", args: { name: "张三" } }工具参数未做标准化(如“张三” vs “张三先生” vs “张*”),导致 DB 查询失败在工具调用前,增加实体标准化钩子args.name = normalizeName(args.name); // 去除称谓、统一编码
Cloudflare Workers 环境下,Harness 启动报ReferenceError: AbortController is not definedReferenceError: AbortController is not definedCF Workers 的 runtime 版本低于 2022.12,不支持 AbortController升级 Workers runtime,或 polyfillglobalThis.AbortController = require('abort-controller');
模型返回中文,但前端显示乱码“某些文本”raw_response: "某些文本"Node.js 默认用 latin1 编码读取 fetch body,未指定 utf-8显式指定 encodingconst text = await response.text(); // 自动 utf-8const buffer = await response.arrayBuffer(); const text = new TextDecoder('utf-8').decode(buffer);
日志里大量parsing_error,但人工看 raw_response 是合法 JSONraw_response: "{\n \"intent\": \"order_inquiry\"\n}"模型返回的 JSON 末尾有不可见字符(如 U+200B 零宽空格),JSON.parse 失败清洗时移除所有零宽字符cleaned = cleaned.replace(/[\u200B-\u200F\u2028\u2029\u202A-\u202E\u2060-\u2064\u2066-\u206F]/g, '');
Agent 在移动端 Safari 上白屏TypeError: undefined is not an object (evaluating 'fetch')Safari 旧版本不支持 top-level await,导致 Harness 初始化失败用 IIFE 包装初始化逻辑(async () => { await initHarness(); })();
模型返回{"response": "好的,已为您查询到:张三,报销单号 BX202405001,金额 ¥3,200.00"},但业务需要结构化数据parsed: { response: "好的,已为您查询到..." }模型未按 schema 输出,但 parsing fallback 值被忽略在 fallback 逻辑中,增加 NLP 提取作为兜底if (parsed.response && !parsed.entities) { parsed.entities = extractEntities(parsed.response); }
多个用户同时操作,tool_call参数混淆tool_call: { name: "get_order_status", args: { orderId: "ORD123" } },但实际查了 ORD456共享变量未隔离,args对象被多个请求引用修改每次调用 deep clone argsconst safeArgs = JSON.parse(JSON.stringify(task.args));
Harness 在 Lambda 上冷启动耗时 8s,超时duration_ms: 8200,stage: "initialization"config 加载、zod schema 编译、tiktoken encoder 初始化都在 handler 内将所有初始化移到 handler 外部const encoder = getEncoding("cl100k_base"); // 顶层

4.2 独家避坑技巧:来自血泪经验的三条铁律

铁律一:永远不要相信模型返回的任何东西,包括它的“成功”状态

我们曾在线上发现,某次模型 API 返回 HTTP 200,但 body 是空字符串""。我们的data.choices?.[0]?.message?.content取出来是undefined,然后JSON.parse(undefined)直接 throw。修复方案不是加一层if (data.choices?.[0]?.message?.content),而是:

// 在 modelCaller.ts 中 const rawResponse = data.choices?.[0]?.message?.content || ''; if (!rawResponse.trim()) { throw new Error('Model returned empty content'); }

模型的“成功”只是 HTTP 层面的,业务层面的成功,必须由 Harness 用业务规则定义。

铁律二:工具调用的 timeout,必须比其依赖的下游服务 timeout 至少短 200ms

比如,你的get_order_status工具调用一个内部 API,该 API 的 SLA 是 800ms。如果你给工具设 timeout 为 800ms,那么当该 API 在 799ms 返回超时,Harness 也会在 800ms 超时,两者几乎同时发生,根本来不及做熔断或 fallback。我们一律设为downstream_timeout - 200,留出缓冲。

铁律三:日志的 request_id,必须从入口请求开始,贯穿所有异步分支

Node.js 的async_hooks在生产环境有性能损耗,我们用更简单的方式:在 Express middleware 中生成req.id = crypto.randomUUID(),然后在所有fetchsetTimeoutPromise.then的回调里,手动传递这个 id。Cloudflare Workers 用env.REQUEST_ID。没有 request_id 的日志,等于没有日志。

提示:在console.log前,永远先console.log({ request_id: req.id, ... })。别指望事后 grep。

5. 工程视角下的 Agent 未来:Harness 将成为新的基础设施层

聊完技术细节,想说点更本质的东西。过去十年,Web 工程经历了从 jQuery 到 React/Vue,再到 Serverless 的演进,每一次,都有一层新的“胶水”成为标配:Webpack 打包、Babel 编译、ESLint 校验、Jest 测试——它们不直接产出业务价值,但没有它们,现代 Web 工程寸步难行。

Harness 正在成为 AI 工程的这一层。它不会出现在任何论文里,但会出现在每个靠谱的 Agent 项目的src/harness/目录下。它不解决“模型能不能懂”,它解决“模型懂了之后,系统能不能稳、能不能快、能不能修、能不能管”。

我们团队内部已经把 Harness 模块化、产品化。现在新项目启动,第一步不是选模型,而是git clone harness-template,填好 config,跑通npm run dev,一个具备基本可观测性、重试、解析、工具

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询