news 2026/8/23 19:01:09

自己动手写Agent Harness【hooks】:生命周期钩子实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
自己动手写Agent Harness【hooks】:生命周期钩子实现

写在前面:系列是为了帮助大家更好的去理解Agent Harness基础设施,并不是想重复造轮子,真实开发建议选择一个成熟的SDK或Harness框架,才是最合适的选择~

1. 引言:skill 和 rule 都不是强制层,底线靠谁

上一篇《自己动手实现一个 agent:skills 与 rules 机制》把话说到一半:skill 管「能干什么」,rule 管「不能碰什么」,但它俩都是触发时才加载的建议层——模型可以不听。那不能妥协的底线靠谁?靠这篇拆的 hooks。钩子把「纪律」从「让 AI 自觉」变成「由工具强制执行」。skill 和 rule 是建议,模型「记得的时候」遵守;hook 是脚本,挂上就一定执行,不由模型自觉。一句话点题:AI 不记得你说过什么,但 hook 记得。

举个具体的。系列先导篇第 3 篇《动手开发你的第一个 agent 03:给它划安全边界》里,我把审批写死进流水线——Approver 按档位 decide,danger 档一律 deny。当时这么干没问题,但我越用越别扭:拦什么、放什么,本质是「策略」;策略要能随时改、随时换,不该固化在 harness 主体里,更不该靠模型当次「想起来」。这篇就把写死的审批拆出来,改造成可编程的钩子。

2. 先看两个成熟 harness:Claude Code 的钩子、dsh 的流水线

动手前先看看两个成熟 harness 把「纪律」放在哪。

Claude Code 把纪律做成一堆生命周期钩子。官方文档(写作日 WebSearch 核实,code.claude.com/docs/en/hooks)列出的钩子事件有二十多个,不同版本数目会浮动。按触发频率分三组:每会话一次的 SessionStart / SessionEnd、每回合一次的 UserPromptSubmit / Stop、每次工具调用一次的 PreToolUse / PostToolUse。钩子本身是脚本(也可以是一个 HTTP 端点、一个 MCP 工具、一段 LLM 提示词),挂上就由 Claude Code 在对应节点自动执行。

最常用的两个,正好一前一后:

  • PreToolUse:工具执行前跑。能拦(返回 deny 就阻止调用)、能改(updatedInput改写工具参数)。它管的是「出去的调用」——模型要调 Bash 之前,钩子可以先看一眼命令是不是rm -rf
  • PostToolUse:工具成功后跑。能记(记录结果)、能改(updatedToolOutput替换模型看到的输出)、能 block。它管的是「回来的结果」——工具跑完了,钩子校验一下输出对不对、记一笔账。

matcher 按工具名过滤(比如只对 Bash 拦),还能用 if 条件按参数过滤(比如只对rm *拦)。

dsh 把纪律放在六阶段工具流水线里。我在《Agent Harness 架构到底需要些什么》拆过 dsh:工具调用要过一条显式的六阶段流水线——prepareExecution(pre-execute waterfall + 审批询问 + 单调守卫)→ tools/execute(timeout/retry)→ 工具 body → createSuccessResult → postExecute(post-execute waterfall)→ finalizeContent + tools/result。权限门禁挂 pre-execute,结果转换挂 post-execute——两道口,一前一后。

两家一个靠钩子、一个靠流水线,做法不同,落点一样:工具调用前有一道闸、调用后有记录。我把它压成一句:最少要有 pre 和 post 两道口,流水线可以短,不能没有。

3. Demo 先行:事件发布 + 钩子注册表

理论收住,动手。给配套工程加一层钩子,代码在examples/first-agent/step7-hooks/index.js,零依赖,node直接跑。

先贴核心:钩子注册表。它干两件事——hook(eventName, fn)注册处理函数,emit(eventName, context)在流水线关键节点广播。外部往注册表里「插」策略,harness 主体不认识它们。

// ============ 钩子注册表(HookRegistry)============// 事件发布 + 注册表:外部通过 hook(eventName, fn) 注入拦截 / 记录函数。// emit(eventName, context) 在流水线关键节点广播,把控制权交给注册的钩子。// 这就是「把纪律写进工具」的落点:策略是一组外部函数,harness 主体不认识它们。exportclassHookRegistry{constructor(){this.handlers=newMap()// 事件名 -> Set<fn>}// hook(eventName, fn):注册钩子,返回「卸载函数」。// 卸载函数是这次设计的关键:钩子不是永久生效的,策略可以装上也可以拆下。hook(eventName,fn){if(!this.handlers.has(eventName))this.handlers.set(eventName,newSet())this.handlers.get(eventName).add(fn)return()=>this.handlers.get(eventName)?.delete(fn)}// emit(eventName, context):广播事件。// 返回 { allow: true },或 { allow: false, reason }(有人拦截)。// 对可拦截事件,任何一个钩子返回 { allow: false, reason } 就短路返回// (fail-fast),后续钩子不再执行;若钩子返回 { allow: true, args },// 则把 args 透传给流水线,实现「改写参数」。// 真实工程:emit 不应在遇 deny 时立即短路整个广播——真实 harness 会让钩子链按注册顺序// await 逐个执行,每个钩子 try/catch 隔离:一个钩子抛错不影响后续钩子与流水线主体;// 还会给钩子加超时,防止某个钩子卡死整个 loop。本 demo 简化为「遇 deny 短路返回」。asyncemit(eventName,context){constfns=this.handlers.get(eventName)if(!fns||fns.size===0)return{allow:true}letrewrittenArgsfor(constfnoffns){constres=(awaitfn(context))??{allow:true}if(res.allow===false){return{allow:false,reason:res.reason??`blocked by${eventName}`}}if(res.args!==undefined)rewrittenArgs=res.args// 最后一个改写参数的钩子生效}return{allow:true,...(rewrittenArgs!==undefined?{args:rewrittenArgs}:{})}}}

注册表有了,把广播点埋进执行流水线。对比系列先导篇第 2 篇《动手开发你的第一个 agent 02:给它装手和眼睛》那条 pre/execute/post 流水线,改动就一处:pre 和 post 不再写死逻辑,改成 emit 两个事件,把控制权交给钩子。新增一个 ToolBlocked 事件,专门给「被拦」记账。

exportclassToolPipeline{constructor(registry,hooks){this.registry=registrythis.hooks=hooks// 事件发布依赖的钩子注册表}asyncrun(toolCall){consttool=this.registry.get(toolCall.name)if(!tool)return{ok:false,error:`unknown tool:${toolCall.name}`}// 1) PreToolUse:工具执行前广播。钩子可以拦,也可以改写参数。constpre=awaitthis.hooks.emit('PreToolUse',{toolCall,tool})if(!pre.allow){// 被拦:工具 execute 根本没被调用(对照 step3 的 deny,但策略在外面)。constblocked={ok:false,blocked:true,reason:pre.reason,error:`pre-hook blocked:${pre.reason}`,}// ToolBlocked:专门给「记账」的时机——PostToolUse 语义是「用过了」,// 这次没用到,所以单独广播,让拦截这件事也有现场。awaitthis.hooks.emit('ToolBlocked',{toolCall,reason:pre.reason,result:blocked})returnblocked}// 真实工程:allow/deny/args 的返回形状是本 demo 自定义的约定——真实 harness 会定义标准// 事件协议(如 PreToolUse 事件对象含 input 可改、决定 allow/deny),让第三方钩子按协议接入。// pre 钩子还能改写参数:返回 { allow: true, args }(如归一化路径、补默认值)。constargs=pre.args??toolCall.arguments// 2) execute:真做。失败不崩,转成结果对象。letvaluetry{value=awaittool.execute(args)}catch(err){constfailed={ok:false,error:`${tool.name}execute failed:${err.message}`}awaitthis.hooks.emit('PostToolUse',{toolCall:{...toolCall,arguments:args},result:failed})returnfailed}constresult={ok:true,content:typeofvalue==='string'?value:JSON.stringify(value,null,2)}// 3) PostToolUse:工具执行后自动广播——记账 / 校验都挂这儿。awaitthis.hooks.emit('PostToolUse',{toolCall:{...toolCall,arguments:args},result})returnresult}}

这段代码值得读三遍。第一遍看结构:run()只在正确的位置广播事件,拦不拦、记不记,全由外部钩子决定。第二遍看那个 ToolBlocked:PostToolUse 的语义是「用过了」,被拦的调用根本没执行,不该硬塞给 PostToolUse,所以单独广播,让「拦截」这件事也有现场。第三遍看 loop 的透明性——ReactLoop 完全不感知钩子,被拦的工具对 loop 来说就是一次失败的工具调用,照常回写。

然后挂上演示钩子。pre 这边两个:安全策略拦危险命令、参数改写做路径归一化。

// ============ 演示钩子 1:安全策略(PreToolUse)============// 这就是「把纪律写进工具」:模型提议什么,钩子审一遍再放行。// 危险模式清单:这类命令大多不可逆(rm -rf 全家桶),模型一旦提议就拦。constDANGEROUS_PATTERNS=['rm -rf','rm -r','mkfs','dd if=','shutdown','reboot','format','rd /s','del /s']functionsafetyPreHook({toolCall}){if(toolCall.name!=='run_command')return{allow:true}constcmd=String(toolCall.arguments?.command??'')// 第一层:命中危险模式,直接拦constmatched=DANGEROUS_PATTERNS.find((p)=>cmd.includes(p))if(matched){return{allow:false,reason:`危险命令模式「${matched}」被策略拦截`}}// 第二层:即便命令看起来无害(如 whoami),命令执行类工具也默认禁止——// 命令该由人工审批,而不是让模型直接执行。这就是「拦截危险命令类」。return{allow:false,reason:'命令执行类工具默认禁止:命令应由人工审批,不交给模型直接执行'}}// 真实工程:审批与钩子并存、职责分开——审批走审批服务(一次 fail-closed,定「该不该做」),// 钩子走扩展点(定「接进来干什么」);step3 的三档审批不会被钩子取代,二者叠加使用。// ============ 演示钩子 2:参数改写(PreToolUse)============// pre 钩子不只拦截,还能改写参数。这里演示归一化:把 './x' 写成 'x'。// 真实系统里常拿它做:路径归一化、密钥脱敏、补默认值、注入租户 ID。functionpathNormalizePreHook({toolCall}){constpath=toolCall.arguments?.pathif(toolCall.name==='read_file'&&typeofpath==='string'&&path.startsWith('./')){constfixed=path.slice(2)console.log(`[pre-hook] 参数改写:${path}${fixed}`)return{allow:true,args:{...toolCall.arguments,path:fixed}}}return{allow:true}}

post 这边一个记账,外加一个回合观测。两个账本:audit 记「真的执行了的」,blockedLog 记「被拦下来的」。

// ============ 演示钩子 3:审计记录(PostToolUse + ToolBlocked)============// 两个账本:audit 记「真的执行了的」,blockedLog 记「被拦下来的」。// 有 pre 拦、有 post 记,出了事才有现场可以追溯——这就是两道口缺一不可的体现。constaudit=[]constblockedLog=[]functionauditPostHook({toolCall,result}){audit.push({name:toolCall.name,args:toolCall.arguments,ok:result.ok,brief:result.ok?String(result.content).slice(0,30):result.error,})console.log(`[post-hook] 记录:${toolCall.name}${result.ok?'ok':'ERR'}(累计${audit.length}次)`)}functionblockedHook({toolCall,reason}){blockedLog.push({name:toolCall.name,args:toolCall.arguments,reason})console.log(`[blocked-hook] 拦截:${toolCall.name}${JSON.stringify(toolCall.arguments)}——${reason}`)}// ============ 演示钩子 4:回合观测(TurnEnd)============// 回合级钩子不拦任何东西,只做「可观测」——追踪、计数、打点。// 这就是广播点不只工具前后、还要有回合前后的原因:挂了钩子,// 整个生命周期都看得见,而 loop 一行不用改。functionturnEndHook({userText,turns}){console.log(`[turn-hook] 回合结束:${userText}→ 用了${turns}个 step`)}

main()里组装——钩子是「插」进来的,拦什么、记什么全在外部策略里,流水线一行没改:

// 真实工程:钩子注册表是内存态,这里 hook() 一注册、重启即失——真实 harness 用配置驱动,// 从配置文件(如 .claude/settings.json 的 hooks 段)加载钩子定义,运行时无需改代码。consthooks=newHookRegistry()hooks.hook('PreToolUse',safetyPreHook)// 安全策略:拦命令执行类 + 危险模式hooks.hook('PreToolUse',pathNormalizePreHook)// 参数改写:归一化路径hooks.hook('ToolBlocked',blockedHook)// 记账:被拦下来的调用hooks.hook('PostToolUse',auditPostHook)// 记账:每次真的执行了的调用hooks.hook('TurnEnd',turnEndHook)// 可观测:回合生命周期

跑起来。本机真实输出(逐字取自step7-hooks/PRACTICE.md,Windows 11 / Node v22.12.0 / mock 模型):

$ node step7-hooks/index.js === 场景 1:放行 —— read_file 正常执行,post 钩子每次记账 === [user] 读文件 package.json [post-hook] 记录:read_file → ok(累计 1 次) [tool:read_file] -> ok [turn-hook] 回合结束:读文件 package.json → 用了 2 个 step [assistant] 工具 read_file 返回了:{ "name": "first-agent", "version": "0.1.0", "private"… === 场景 2:被拦 —— 命令执行类工具,pre 钩子拦截(不进执行)=== [user] 执行命令 whoami [blocked-hook] 拦截:run_command {"command":"whoami"} —— 命令执行类工具默认禁止:命令应由人工审批,不交给模型直接执行 [tool:run_command] -> BLOCKED(命令执行类工具默认禁止:命令应由人工审批,不交给模型直接执行) [turn-hook] 回合结束:执行命令 whoami → 用了 2 个 step [assistant] 工具 run_command 返回了:pre-hook blocked: 命令执行类工具默认禁止:命令应由人工审批,不交给模型直接执行 === 场景 3:被拦 —— 直接调用 pipeline,危险模式匹配 === [blocked-hook] 拦截:run_command {"command":"rm -rf /"} —— 危险命令模式「rm -rf」被策略拦截 直接 run_command(rm -rf /) → 被拦:危险命令模式「rm -rf」被策略拦截 === 场景 4:改写 —— pre 钩子归一化路径后再执行 === [pre-hook] 参数改写:./package.json → package.json [post-hook] 记录:read_file → ok(累计 2 次) 直接 read_file(./package.json) → ok(路径已被改写为 package.json) === 纪律的证据:两个账本 === · audit —— 真的执行了的工具调用(post 钩子记账): - read_file path=package.json → ok - read_file path=package.json → ok · blocked —— 被拦下来的调用(ToolBlocked 钩子记账): - run_command command=whoami → 命令执行类工具默认禁止:命令应由人工审批,不交给模型直接执行 - run_command command=rm -rf / → 危险命令模式「rm -rf」被策略拦截

四个场景,逐行拆。场景 1 是放行:read_file 正常执行,post 钩子每次记一笔。场景 2 最关键:mock 提议run_command whoami,pre 钩子拦下(BLOCKED),工具 execute 没跑,拦截被 ToolBlocked 记账,loop 照常继续、模型读到「被拒」后总结收尾——这行就是「纪律由工具强制执行」的现场,不是模型自觉。场景 3 是直接调 pipeline 测危险模式。场景 4 证明 pre 不只拦还能改:./package.json被改写成package.json,改写后的参数进了 audit 账本。

自测也跑一遍:

$ node step7-hooks/test.js ✅ ① pre 钩子拦截时:工具不执行,且调用被记录为拦截 ✅ ② post 钩子在每次工具执行后触发 ✅ ③ 钩子可卸载;未匹配的工具调用不受影响 ✅ ④ 被拦调用以 toolResult 回写 loop 历史(模型能读到「被拒」) 全部通过:pre 拦得住、post 记得到、钩子可装卸——纪律由工具强制执行。

test.js 里的哨兵手法值得抄:fragileExecuted是外部状态哨兵,fragile 一旦 execute 就置 true。断言它还是 false,证明「被拦截时工具 execute 绝不能被调用」——不是返回失败,是根本没进执行。

想换真实模型?照工程 README 设OPENAI_BASE_URL/OPENAI_API_KEY/OPENAI_MODEL三个环境变量就自动切到llm/real.js,mock/real 同一个complete(messages, tools)签名,换 provider 一行改。真实 API 我本机没跑(无 key),标「待核实」。

4. 两个核心设计:事件发布与钩子注册表

拆开看,这套东西就两个设计,各管一摊。

设计一:事件发布——loop 在关键节点广播。广播点分两级:回合级(TurnStart / TurnEnd)和工具级(PreToolUse / PostToolUse / ToolBlocked)。先给一张定位图,把广播点标出来。

图里一个细节值得单独说:为什么广播点放在流水线和 turn 的边界,而不是 execute 里?因为 execute 是工具 body,是「干活」的地方;纪律要加在「干活之前」和「干活之后」。loop 完全不感知钩子,是这套设计最顺的地方——纪律加在流水线上,loop 一行不用改。

设计二:钩子注册表——策略是外挂的。hook(eventName, fn)注册、返回卸载函数;emit遍历处理函数,任何一个返回{ allow: false }就短路。两个要点:

  • 拦截是短路,改写是透传。拦截是 allow/deny 二元,任何一个钩子返回 deny 就 fail-fast;改写不一样,钩子返回{ allow: true, args },emit 要把它透传出去。这个点我踩过坑:一开始 emit 只返回 allow/deny,pre 钩子想改参数时 args 被丢掉,流水线读到的永远是原始参数。修正:emit 收集最后一个 args,折进返回值,流水线再pre.args ?? toolCall.arguments。这是个很容易漏的设计——「拦截」是二元,「改写」需要额外通道。
  • 卸载是必需。hook()返回卸载函数,策略可以装上也可以拆下。test ③ 固化了这个行为。

最后说「复用审批三档」。safetyPreHook里有两层判断,都是系列先导篇第 3 篇审批三档的翻版:危险模式清单是 denylist 一票否决;「命令执行类工具默认禁止」是把 run_command 挂到 danger 档——命令该由人工审批,不交给模型直接执行。区别只在:第 3 篇的 approve 是硬编码进流水线,这里策略是外挂的,要改拦什么,改函数就行,流水线一行没动。

还有一条设计纪律必须讲:被拦的结果必须回写 loop 历史。若被拦只返回、不回写 toolResult,mock 这种确定性模型下一步会再提同一个run_command,一路撞满 maxTurns=10 停机。把被拦作为{ role:'toolResult', content: error }回写(与第 3 篇的 deny 回写同源),模型读到「被拒」就改口。test ④ 专门固化了这个行为——我猜你踩坑时最可能漏掉的就是这条。

demo 是教学最小版,有几处简化,真实工程得补,我逐条说。第一,emit 是同步短路:一个钩子 deny 就停,异常也不隔离;真实 harness 让钩子链按注册顺序逐个 await,每个钩子 try/catch 包住,一个抛错不影响后续钩子与流水线主体,还带超时防某个钩子卡死 loop。第二,我只挂了 5 个事件点,真实工程全生命周期都可挂——会话开始/结束、用户输入提交、每次模型回复、工具调用前后、配置加载后,Claude Code 就有 SessionStart/SessionEnd/UserPromptSubmit/Stop 等 20+ 个。第三,注册方式:我在代码里 hook() 注册、内存态重启即失,真实 harness 用配置驱动,从 .claude/settings.json 的 hooks 段加载钩子定义,运行时不用改代码。第四,allow/deny/args 返回形状是我自定义的约定,真实工程会定标准事件协议,像 PreToolUse 事件对象含 input 可改、决定 allow/deny,第三方钩子按协议接入。第五,我用钩子模拟了先导篇第 3 篇的审批语义,真实工程把两者拆开——审批走审批服务(一次 fail-closed,管「该不该做」),钩子走扩展点(管「接进来干什么」),叠加使用、互不取代。

5. 对照 dsh:六阶段流水线的 pre/post 最小投影

第四节那套东西,对照 dsh 的六阶段流水线,一眼看穿你写的是什么。

两道口,就是你emit('PreToolUse')emit('PostToolUse')的位置。dsh 六阶段里,你的 pre 口对应 prepareExecution 的 pre-execute waterfall(拦截、审批、守卫挂这),你的 post 口对应 postExecute 的 post-execute waterfall(接受、阻断、替换、附加上下文),你的 execute 对应中间那两段。

我压成一句:你的钩子注册表,是 dsh 六阶段流水线 pre/post 两道口的最小投影。你只实现了「广播 + 注册」,dsh 在这两道口之间还塞了审批询问、单调守卫、三个瀑布、finalizeContent——但位置和意图一模一样。这就是「白给的架构能力」:你没抄 dsh 的实现,抄的是它的站位。

这也回应了我在《拆三家》下的落点:工具流水线最少要有 pre 和 post 两道口。只有 pre 没有 post:拦得住坏事,但「到底发生了什么」没人记账,出了事没有现场。只有 post 没有 pre:只能事后发现事故,拦不住。两道口都有:事前能拦、事后能记——纪律闭环。Claude Code 的 PreToolUse/PostToolUse、dsh 的 pre/post 两个 waterfall,都至少保留这两道口,就是这个原因。

一个小差异,诚实说。Claude Code 把「工具失败」单独拆成 PostToolUseFailure 事件;我的最小版偷懒了——execute 抛错时也走 PostToolUse,只是result.ok=false。想学 Claude Code 拆开,加一个 PostToolUseFailure 事件就行,广播点的位置我已经留了。

呼应一句旧文。在《复杂软件系统的 Vibe Coding 实践三:工具配置与迭代收尾》里,我把 hook 用在 TDD 上:PreToolUse 拦「没先写失败测试就写生产代码」,PostToolUse 写完代码自动跑测试。那是在 Claude Code 的配置里用的现成机制——当时我是用户,现在你自己造出来了同一个机制。Claude Code 的钩子是产品功能,你的钩子是你 harness 里的接缝:前者给你配,后者你来定。

6. 结论:钩子=把纪律写进工具,AI 不记得你记得

回顾这一篇:给 harness 加了事件发布 + 钩子注册表,把写死的审批改造成可编程的钩子,跑通了四条真实行为——pre 拦得住、post 记得到、钩子可装卸、被拦能回写。

钩子把「纪律」从「让 AI 自觉」变成「由工具强制执行」。skill 和 rule 是建议,模型可以不听;hook 是脚本,挂上就一定执行。策略写在 hook 里,只要 hook 在,它就永远生效——不依赖模型当次有没有「想起来」。

流水线可以短,不能没有。最少要有 pre 和 post 两道口:pre 管拦截、审批、改写输入;post 管记录、校验、改写结果。你的 harness 已经长到第七格,子代理、技能与规则、钩子都装上了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/23 18:58:42

SpringBoot+Vue毕业生招聘平台开发实践

1. 项目概述 1.1 系统背景与意义 作为一名长期从事Java全栈开发的工程师&#xff0c;我最近完成了一个基于SpringBoot的毕业生信息招聘平台项目。这个系统的开发源于当前高校毕业生就业过程中存在的信息不对称问题。传统的校园招聘往往受限于时间和空间&#xff0c;而各类招聘…

作者头像 李华
网站建设 2026/8/23 18:58:04

嵌入式开发中IAR链接文件(.icf)配置详解与内存布局实战

1. 项目概述&#xff1a;链接文件&#xff0c;嵌入式开发的“城市规划图”如果你用IAR Embedded Workbench开发过ARM Cortex-M芯片&#xff0c;那你一定在工程里见过那个后缀为.icf的文件。很多新手第一次看到它&#xff0c;会直接忽略&#xff0c;或者从别的工程里复制一个过来…

作者头像 李华
网站建设 2026/8/23 18:50:35

机器学习实战:从数据工程到模型部署的完整工程化指南

1. 从“知道”到“做到”&#xff1a;为什么你的机器学习项目总在起点徘徊&#xff1f;我们可能都经历过这样的场景&#xff1a;在Coursera或者B站上刷完了吴恩达的《机器学习》课程&#xff0c;对梯度下降、逻辑回归、支持向量机这些名词如数家珍&#xff0c;甚至能在白板上推…

作者头像 李华
网站建设 2026/8/23 18:49:58

C++函数模板的局限性:从万能钥匙到特定锁芯的实践思考

1. 从“万能钥匙”到“特定锁芯”&#xff1a;重新审视函数模板在C的日常开发中&#xff0c;函数模板&#xff08;Function Template&#xff09;常常被我们视为一把“万能钥匙”。当我们需要一个能处理多种数据类型的max函数、swap函数或者排序算法时&#xff0c;第一个想到的…

作者头像 李华
网站建设 2026/8/23 18:49:47

数学建模竞赛实战:数据挖掘与机器学习在考古玻璃成分分析中的应用

1. 项目背景与核心挑战&#xff1a;当数学建模遇见考古学去年带队参加全国大学生数学建模竞赛&#xff0c;选的就是这道C题。说实话&#xff0c;当时看到“古代玻璃制品成分分析与鉴别”这个标题&#xff0c;我们团队三个人都愣了一下。这和我们预想的“交通流量优化”、“疫情…

作者头像 李华