news 2026/10/1 13:35:27

Genkit代理API实战:TypeScript+Firestore构建多回合AI客服代理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Genkit代理API实战:TypeScript+Firestore构建多回合AI客服代理

1. 从单次问答到多回合代理:为什么需要Genkit的代理API

很多人做AI应用的第一步,都是接一个大模型接口,然后写一个chat函数,把用户输入丢进去,拿回结果渲染到页面上。这个模式在演示阶段没问题,但一旦你想做点真正有用的东西——比如一个能帮用户查订单、改地址、退换货的客服助手,或者一个能根据用户需求逐步推荐旅行方案的规划工具——单次问答立刻就不够用了。

问题出在“状态”上。单次问答是无状态的,每次请求都是独立的,模型不知道上一轮用户说了什么,也不知道自己刚才承诺了什么。你当然可以把历史消息拼成一个长字符串再发给模型,但这样做有几个硬伤:第一,上下文窗口很快会被撑爆;第二,模型无法主动调用外部工具去获取实时数据;第三,你没法控制对话的走向,模型可能在第3轮就忘了第1轮用户提到的关键约束。

多回合AI代理要解决的就是这些问题。它需要具备三个核心能力:记住对话历史、在合适的时候调用外部工具、根据工具返回的结果继续推理并生成回复。Genkit的代理API就是为这个场景设计的。它提供了一套结构化的方式来定义代理的行为、管理对话状态、注册工具函数,并且把整个流程串起来。

我这次用Genkit的代理API配合TypeScript和Firestore,搭了一个多回合的客服代理原型。选TypeScript是因为Genkit对TS的支持最完整,类型推导能帮你在写工具函数时少踩很多坑;选Firestore是因为它天然适合存对话历史,而且和Genkit的会话管理能对上。下面我把整个搭建过程拆开讲,包括设计思路、关键代码、踩过的坑,以及一些文档里不会写的实操细节。

这篇文章适合两类人看:一是已经用过Genkit做单次生成、想进一步做多回合代理的开发者;二是正在选型多回合对话框架、想看看Genkit的代理API到底好不好用的人。如果你还没接触过Genkit,建议先花半小时跑通它的generate基础示例,再来看这篇,否则有些概念会有点跳。

2. 整体架构设计与核心思路拆解

2.1 为什么选Genkit的代理API而不是自己拼Prompt

自己拼Prompt做多回合对话,本质上是在做一件事:把历史消息序列化成文本,塞进下一次请求的上下文里。这个方案在对话轮次少、工具调用简单的时候能跑,但一旦轮次超过10轮,或者需要模型在多个工具之间做选择,维护成本会指数级上升。

Genkit的代理API把这件事抽象成了几个明确的组件。Agent定义代理的身份和行为,Session管理对话状态,Tool注册可调用的外部函数,Flow把整个执行链路串起来。你不需要手动拼历史消息,Genkit会在每次调用时自动把当前会话的上下文注入进去。你也不需要写复杂的路由逻辑来决定什么时候调工具,模型会根据工具的description和参数schema自己判断。

我选这个方案的核心原因是:它把“状态管理”和“工具调用”这两件最容易写乱的事,变成了框架层面的约定。你只需要关注业务逻辑——用户想干什么、需要查什么数据、返回什么格式——剩下的交给Genkit。

2.2 Firestore在架构中的角色与数据模型设计

Firestore在这个架构里承担的是会话持久化的角色。Genkit本身有内存级的会话存储,但进程重启就没了,不适合生产环境。Firestore的好处是:写入延迟低、支持实时监听、按文档组织数据天然适合会话这种结构。

我设计的Firestore数据模型是这样的:

  • sessions集合:每个文档对应一个会话,文档ID就是sessionId
    • userId:用户标识
    • createdAt:会话创建时间
    • lastActiveAt:最后活跃时间
    • status:会话状态(active / closed)
  • sessions/{sessionId}/messages子集合:每条消息一个文档
    • role:user / assistant / tool
    • content:消息内容
    • toolCalls:如果是工具调用消息,记录调用的工具名和参数
    • timestamp:消息时间

这个模型的好处是:消息按时间顺序排列,读取时直接按timestamp排序就行;子集合的结构让单个会话的消息不会影响其他会话的查询性能;lastActiveAt可以用来做会话过期清理。

注意:Firestore的子集合查询需要建立索引,如果你要按role过滤消息,记得在Firestore控制台里给messages子集合加一个role+timestamp的复合索引,否则查询会报错。

2.3 多回合代理的状态流转逻辑

多回合代理的状态流转,核心是回答三个问题:当前会话处于什么阶段?用户这句话触发了什么意图?代理下一步该做什么?

我的设计是把状态分成两层。会话层状态存在Firestore里,包括对话历史、用户信息、当前任务上下文。执行层状态由Genkit的Agent在单次调用中管理,包括当前这轮需要调用哪些工具、工具返回后如何继续推理。

流转逻辑是这样的:用户发消息 → Genkit加载会话历史 → Agent根据历史+当前输入决定是否调用工具 → 如果需要调工具,执行工具函数 → 把工具结果注入上下文 → Agent继续推理 → 生成最终回复 → 把用户消息和代理回复写入Firestore。

这个流程里最关键的是工具调用的决策点。Genkit的代理API会让模型自己判断是否需要调工具,你只需要在定义工具时把description写清楚。比如“查询订单状态”这个工具,description要写成“当用户询问订单物流、配送进度、预计到达时间时调用此工具”,而不是简单的“查订单”。description的质量直接决定了模型会不会在正确的时机调工具。

3. 核心细节解析与实操要点

3.1 环境准备与依赖安装

先把基础环境搭起来。我用的Node.js 20 LTS,TypeScript 5.3,Genkit的版本是0.9.x。依赖清单如下:

npm init -y npm install genkit @genkit-ai/googleai @genkit-ai/firebase firebase-admin npm install -D typescript tsx @types/node

genkit是核心包,@genkit-ai/googleai提供Gemini模型的接入,@genkit-ai/firebase提供Firestore的会话存储适配,firebase-admin用来初始化Firestore连接。

TypeScript配置里需要把strict打开,因为Genkit的工具定义依赖类型推导,strict模式下能提前发现参数类型不匹配的问题。tsconfig.json关键配置:

{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "outDir": "dist" }, "include": ["src/**/*.ts"] }

实操心得:Genkit的某些类型定义在moduleResolution: "bundler"下会有解析问题,建议直接用NodeNext。如果你用的是monorepo,确保genkit包在根目录的node_modules里,否则类型推导会失效。

3.2 定义Agent与工具函数的关键细节

定义Agent的时候,最重要的是把systemPrompt写清楚。这个prompt决定了代理的角色、行为边界和回复风格。我的客服代理systemPrompt大概是这样:

你是一个电商客服助手,负责处理订单查询、退换货和地址修改。 你可以调用工具来获取订单信息,但不要编造任何订单数据。 如果用户的问题超出你的能力范围,引导用户联系人工客服。 回复要简洁,每次只问一个必要的问题,不要一次性抛出多个问题。

工具函数的定义用genkit.defineTool,关键是把name、description、inputSchema、outputSchema都写完整。inputSchema用Zod来定义,这样Genkit能自动做参数校验和类型推导。

import { z } from "genkit"; export const queryOrderTool = ai.defineTool( { name: "queryOrder", description: "当用户询问订单状态、物流进度、预计送达时间时调用此工具。需要提供订单号。", inputSchema: z.object({ orderId: z.string().describe("订单号,通常是10位数字"), }), outputSchema: z.object({ status: z.string(), estimatedDelivery: z.string(), items: z.array(z.string()), }), }, async (input) => { // 实际项目中这里查数据库 return { status: "已发货", estimatedDelivery: "2024-12-20", items: ["无线耳机", "保护壳"], }; } );

这里有个容易忽略的点:outputSchema不是必须的,但强烈建议加上。因为模型需要根据工具返回的结构来决定下一步怎么回复,如果返回的是自由格式的字符串,模型可能会解析错。用结构化schema,模型能更准确地理解返回数据的含义。

3.3 会话存储与Firestore的对接方式

Genkit的会话存储接口是SessionStore,你需要实现get、save、delete三个方法。@genkit-ai/firebase包里已经提供了FirestoreSessionStore,直接拿来用就行:

import { FirestoreSessionStore } from "@genkit-ai/firebase"; import { getFirestore } from "firebase-admin/firestore"; const db = getFirestore(); const sessionStore = new FirestoreSessionStore(db);

然后在初始化Genkit的时候把sessionStore传进去:

import { genkit } from "genkit"; import { googleAI } from "@genkit-ai/googleai"; export const ai = genkit({ plugins: [googleAI()], model: "googleai/gemini-1.5-flash", sessionStore, });

注意:FirestoreSessionStore默认把会话存在genkit_sessions集合里,如果你想用自己的集合名,需要自己实现SessionStore接口。我建议直接用默认的,省事,而且和Genkit的版本升级兼容性更好。

3.4 多回合对话的上下文管理策略

多回合对话最容易出问题的地方是上下文长度。Gemini 1.5 Flash的上下文窗口是1M token,看起来很大,但如果你把每一轮的消息都完整保留,几十轮之后还是会接近上限。而且上下文越长,模型的响应延迟越高,成本也越高。

我的策略是滑动窗口+摘要。保留最近10轮完整对话,更早的对话用模型生成一段摘要,把摘要作为系统消息的一部分注入。这样既保留了关键信息,又控制了上下文长度。

Genkit的Agent API本身不提供摘要功能,需要你自己在写入Firestore之前做一层处理。我的做法是在每次写入新消息之前,检查当前会话的消息数量,如果超过20条,就把最早的10条拿出来,调一次ai.generate生成摘要,然后把摘要存到会话文档的summary字段里,同时删除那10条消息。

这个逻辑写起来不复杂,但要注意摘要的触发时机。不要在每次请求都检查,那样会增加延迟。我是在写入代理回复之后异步触发摘要生成,不阻塞当前请求的返回。

4. 实操过程与核心环节实现

4.1 初始化Genkit与注册代理的完整流程

初始化分三步:配置Firebase Admin、创建Genkit实例、注册Agent和工具。

import { initializeApp, cert } from "firebase-admin/app"; import { getFirestore } from "firebase-admin/firestore"; import { genkit } from "genkit"; import { googleAI } from "@genkit-ai/googleai"; import { FirestoreSessionStore } from "@genkit-ai/firebase"; // 第一步:初始化Firebase initializeApp({ credential: cert({ projectId: process.env.FIREBASE_PROJECT_ID, clientEmail: process.env.FIREBASE_CLIENT_EMAIL, privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"), }), }); const db = getFirestore(); // 第二步:创建Genkit实例 const sessionStore = new FirestoreSessionStore(db); export const ai = genkit({ plugins: [googleAI({ apiKey: process.env.GOOGLE_AI_API_KEY })], model: "googleai/gemini-1.5-flash", sessionStore, }); // 第三步:注册工具 export const queryOrderTool = ai.defineTool({...}, async (input) => {...}); export const updateAddressTool = ai.defineTool({...}, async (input) => {...}); // 第四步:定义Agent export const customerServiceAgent = ai.defineAgent({ name: "customerService", systemPrompt: "...", tools: [queryOrderTool, updateAddressTool], });

这里有个细节:defineAgent的tools数组里放的是工具定义,不是工具函数本身。Genkit会自动把工具的schema暴露给模型,模型根据schema决定调用哪个工具、传什么参数。

4.2 处理一轮完整对话的代码实现

一轮完整对话的处理流程,我封装成了一个handleMessage函数:

export async function handleMessage(sessionId: string, userMessage: string) { // 1. 获取或创建会话 let session = await sessionStore.get(sessionId); if (!session) { session = await sessionStore.create(sessionId, { userId: "user_001", createdAt: new Date().toISOString(), }); } // 2. 把用户消息写入Firestore await db.collection("sessions").doc(sessionId) .collection("messages").add({ role: "user", content: userMessage, timestamp: new Date().toISOString(), }); // 3. 调用Agent生成回复 const response = await customerServiceAgent.run({ sessionId, input: userMessage, }); // 4. 把代理回复写入Firestore await db.collection("sessions").doc(sessionId) .collection("messages").add({ role: "assistant", content: response.text, toolCalls: response.toolCalls || [], timestamp: new Date().toISOString(), }); // 5. 更新会话活跃时间 await db.collection("sessions").doc(sessionId).update({ lastActiveAt: new Date().toISOString(), }); return response.text; }

这段代码里,customerServiceAgent.run是核心。Genkit会自动做几件事:从sessionStore加载会话历史、把历史消息和当前输入拼成模型请求、根据模型的输出判断是否需要调工具、如果需要调工具就执行工具函数、把工具结果注入上下文、再次调用模型生成最终回复。

实操心得:response.toolCalls里记录了这轮对话调用了哪些工具、传了什么参数、返回了什么结果。这个信息在调试的时候非常有用,建议一定要存下来。我遇到过模型调工具时传错参数的情况,靠toolCalls记录才定位到是description写得不够明确。

4.3 工具调用的参数校验与错误处理

工具函数的参数校验,Genkit会用Zod schema自动做。如果模型传的参数不符合schema,Genkit会抛出一个ToolInputError。你需要捕获这个错误,然后决定是让模型重试还是直接返回错误提示。

我的做法是在工具函数内部做一层业务校验,比如订单号格式不对、订单不存在等情况,返回一个结构化的错误对象,而不是抛异常:

async (input) => { if (!/^\d{10}$/.test(input.orderId)) { return { status: "error", message: "订单号格式不正确,请提供10位数字的订单号", }; } const order = await db.collection("orders").doc(input.orderId).get(); if (!order.exists) { return { status: "error", message: "未找到该订单,请确认订单号是否正确", }; } return { status: "success", data: order.data(), }; }

这样模型拿到status: "error"之后,会根据message生成一个友好的回复,引导用户重新提供正确的订单号。比直接抛异常让整个请求失败要好得多。

4.4 会话过期与清理机制的实现

生产环境里,会话不能无限增长。我设置了两条清理规则:活跃会话保留7天,非活跃会话保留24小时。

实现方式是用Firestore的TTL功能,在lastActiveAt字段上设置过期时间。但Firestore的TTL是异步删除,不保证精确时间,所以还需要一个定时任务做兜底清理。

// 每天凌晨3点执行清理 export async function cleanupSessions() { const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const expiredSessions = await db.collection("sessions") .where("lastActiveAt", "<", cutoff) .where("status", "==", "active") .get(); const batch = db.batch(); expiredSessions.docs.forEach((doc) => { batch.update(doc.ref, { status: "closed" }); }); await batch.commit(); }

注意:Firestore的批量操作最多500条,如果过期会话超过500个,需要分批处理。我一般用while循环加limit(500)来分批查、分批删。

5. 常见问题与排查技巧实录

5.1 模型不调用工具或调用错误工具怎么办

这是最常见的问题。模型不调工具,通常是因为工具的description写得太模糊。比如“查询订单”这个description,模型不知道什么时候该用。改成“当用户询问订单物流状态、配送进度、预计送达时间、订单是否已发货时调用此工具”,模型就能准确判断了。

另一个原因是systemPrompt里没有明确告诉模型“你可以调用工具”。有些模型默认倾向于直接回答,而不是调工具。在systemPrompt里加一句“遇到需要查询数据的问题时,优先调用工具获取准确信息,不要凭记忆回答”,能明显提升工具调用率。

如果模型调用了错误的工具,检查两个工具的description是否有重叠。比如“查询订单”和“查询物流”这两个工具,如果description都写得很宽泛,模型就容易混。解决办法是把边界写清楚:查询订单负责返回订单基本信息,查询物流负责返回配送轨迹,两者不要有交叉。

5.2 多回合对话中上下文丢失的排查思路

上下文丢失的表现是:用户在第1轮说了订单号,第3轮模型又问了一遍订单号。排查步骤:

  1. 检查Firestore里messages子集合的消息是否完整。如果消息缺失,说明写入逻辑有问题。
  2. 检查sessionStore.get返回的会话历史是否包含了所有消息。可以在handleMessage里打印一下session.messages.length。
  3. 检查是否触发了摘要逻辑。如果摘要生成有问题,早期消息被删了但摘要没存上,就会导致上下文丢失。
  4. 检查模型的上下文窗口是否超限。如果超限,Genkit可能会截断历史消息,导致早期信息丢失。

我遇到过一次上下文丢失,最后发现是摘要生成时用的模型和主模型不是同一个,摘要的格式和主模型的预期不一致。统一用同一个模型做摘要就解决了。

5.3 Firestore读写延迟与并发冲突的处理

Firestore的写入延迟通常在100ms以内,但如果你在一个请求里连续做多次写入(写用户消息、写代理回复、更新会话时间),总延迟会累积到300-500ms。优化方法是把多次写入合并成一个批量操作:

const batch = db.batch(); const sessionRef = db.collection("sessions").doc(sessionId); batch.set(sessionRef.collection("messages").doc(), userMessageDoc); batch.set(sessionRef.collection("messages").doc(), assistantMessageDoc); batch.update(sessionRef, { lastActiveAt: new Date().toISOString() }); await batch.commit();

并发冲突主要出现在同一个会话同时收到多条消息时。Firestore的事务可以解决这个问题,但事务会增加延迟。我的做法是在应用层加一个简单的锁:用sessionId作为key,在内存里维护一个Map<string, Promise>,同一个会话的请求串行执行。

5.4 常见问题速查表

问题现象可能原因排查方法解决方案
模型不调工具description太模糊检查工具description补充触发场景描述
调错工具工具边界重叠对比多个工具的description明确各自职责范围
上下文丢失摘要逻辑异常检查摘要生成和存储统一摘要模型
写入延迟高多次单独写入检查写入次数合并为批量操作
并发冲突同一会话并发请求检查请求日志应用层加会话锁
工具参数错误schema定义不严检查Zod schema加严校验规则
会话无限增长无清理机制检查会话数量加TTL和定时清理

6. 性能优化与扩展方向

6.1 减少模型调用次数的缓存策略

多回合代理的成本大头在模型调用上。每一轮对话至少调一次模型,如果涉及工具调用,就是两次(一次决定调工具,一次生成最终回复)。优化方向是缓存常见问题的回复。

我的做法是维护一个faq_cache集合,key是用户问题的向量化表示,value是标准回复。用户发消息后,先算一下问题的embedding,在缓存里查相似度超过0.95的,直接返回缓存回复,不走模型。这个策略对客服场景特别有效,因为很多用户问的是重复问题。

Genkit本身有embedding的API,用ai.embed就能生成向量。Firestore不支持向量检索,但可以用firebase-admin的vector扩展,或者把向量存到专门的向量数据库里。我图省事,直接用了一个简单的内存缓存,key是问题的MD5,只缓存完全匹配的问题,命中率大概30%,但已经能省不少成本了。

6.2 多代理协作的架构演进思路

单个代理能处理的问题有限。当业务变复杂时,可以考虑多代理协作。比如一个“路由代理”负责判断用户意图,然后把请求分发给“订单代理”、“退换货代理”、“投诉代理”。

Genkit的代理API支持这种模式。你可以定义多个Agent,然后在路由代理的systemPrompt里告诉它“根据用户意图,调用对应的子代理”。子代理的调用方式和工具调用类似,只是返回的不是数据,而是一段回复。

这种架构的好处是每个子代理的systemPrompt可以写得很专注,工具集也可以更精简,模型的判断准确率会更高。代价是调用链变长,延迟增加。我的经验是:当单代理的工具超过5个,或者systemPrompt超过500字时,就该考虑拆分了。

6.3 从原型到生产的检查清单

原型跑通之后,上线之前需要检查这些项:

  • 会话存储是否从内存切换到了Firestore
  • 是否加了会话过期和清理机制
  • 是否加了错误处理和重试逻辑
  • 是否加了请求日志和工具调用日志
  • 是否加了速率限制,防止单个用户刷接口
  • 是否加了敏感词过滤,防止代理输出不当内容
  • 是否加了成本监控,跟踪每天的模型调用量和token消耗
  • 是否做了压力测试,确认并发量下的响应时间

实操心得:速率限制我建议用express-rate-limit或者类似的中间件,按userId做限流,而不是按IP。因为同一个用户可能从多个设备访问,按IP限流会误伤。限流阈值我设的是每分钟10次对话请求,超过就返回“请求过于频繁,请稍后再试”。

7. 我在实际搭建中踩过的坑与经验总结

第一个坑是Firestore的undefined值问题。Firestore不接受undefined作为字段值,但TypeScript的可选属性默认就是undefined。我在写入消息时,toolCalls字段有时候是undefined,直接写入会报错。解决办法是在写入之前做一次清洗,把undefined转成null或者空数组。

第二个坑是Genkit的会话ID和Firestore文档ID的映射。Genkit的sessionStore.get如果找不到会话,默认行为是返回null,但有些版本的实现会抛异常。我在代码里加了一层try-catch,确保找不到会话时能正常创建新会话,而不是让整个请求失败。

第三个坑是模型的工具调用格式不一致。Gemini 1.5 Flash和Gemini 1.5 Pro在工具调用的返回格式上有细微差别,Flash有时候会把工具调用的参数放在text字段里而不是toolCalls字段里。解决办法是在解析响应时同时检查这两个字段,做兼容处理。

第四个坑是摘要生成的时机。我一开始是在每次请求开始时检查消息数量,超过阈值就生成摘要。结果发现这样会让每轮对话的延迟增加200-300ms。后来改成异步生成,在返回响应之后用setImmediate触发摘要任务,不阻塞主流程,体验好很多。

第五个坑是Firestore的复合索引。我按role和timestamp查询消息时,Firestore报错说缺少索引。去控制台建索引花了10分钟,期间查询一直失败。建议在开发阶段就把可能用到的复合索引都建好,别等到上线才发现。

最后分享一个小技巧:在systemPrompt里加一句“如果用户提供了订单号,先调用queryOrder工具确认订单存在,再进行后续操作”。这句话能显著减少模型在订单不存在的情况下继续瞎编回复的情况。我试过不加这句话,模型会在订单号错误时仍然生成一段“您的订单正在配送中”的假回复,加了之后模型会先调工具,发现订单不存在,然后引导用户重新提供订单号。

这个代理原型我跑了大概两周,处理了2000多轮对话,工具调用的准确率在92%左右,剩下的8%主要是用户输入太模糊导致模型判断困难。后续我打算把路由代理加上,把订单查询和退换货拆成两个子代理,看看准确率能不能再提一提。

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

CentOS 7 EOL后镜像下载、安装、换源与排障实战

CentOS 7 的官方维护周期在 2024 年 6 月 30 日正式走完&#xff0c;这件事直接改变了 centos7 镜像文件下载 的玩法——以前打开官网首页就能顺手拿到的 DVD ISO&#xff0c;现在得绕到归档目录里去翻&#xff0c;很多老教程里的直链早就 404 了。我最近帮朋友重装了几台测试机…

作者头像 李华
网站建设 2026/10/1 13:33:30

Agent记忆系统实战:基于MCP与Docker构建可检索的LLM长期记忆

1. 从“hindsight”这个词说起&#xff1a;为什么它值得单独拿出来聊第一次看到“hindsight”被当作一个项目名&#xff0c;我脑子里蹦出来的不是词典释义&#xff0c;而是一个很具体的场景&#xff1a;你在跟一个 LLM Agent 对话&#xff0c;它前面明明已经确认过“用户偏好用…

作者头像 李华
网站建设 2026/10/1 13:33:07

EMC实战指南:共模电流回路与网口防护电路设计

看到“6.2 EMC”这个编号&#xff0c;很多人的第一反应是教科书目录里的某一节&#xff0c;或者是某份测试报告里的条款号。但在我做了十几年硬件设计和EMC整改之后&#xff0c;越来越觉得这两个词放在一起&#xff0c;代表的绝不是一个孤立的章节&#xff0c;而是一整套从测试…

作者头像 李华
网站建设 2026/10/1 13:32:29

Agent记忆系统实战:基于MCP与Docker的hindsight架构解析

1. 从“hindsight”说起&#xff1a;为什么我们需要给 Agent 装上“后视镜”第一次看到“hindsight”这个词&#xff0c;我脑子里蹦出来的不是词典释义&#xff0c;而是自己踩过的一个坑。去年我搭了一个基于 LLM 的客服 Agent&#xff0c;跑 demo 的时候一切正常&#xff0c;用…

作者头像 李华
网站建设 2026/10/1 13:31:29

自建AI日报流水线:信息筛选与自动化工作流实践

1. 当"AI日报"变成一种日常仪式&#xff1a;我在信息洪流里搭了个筛子每天早上七点半&#xff0c;我端着咖啡坐到工位前&#xff0c;第一件事不是打开邮箱&#xff0c;而是扫一眼自己攒的那份"AI日报"。这个习惯从2024年延续到现在&#xff0c;中间迭代过至…

作者头像 李华
网站建设 2026/10/1 13:31:07

OPNET DSR源码实测:71节点链状拓扑下的可调试动态源路由协议栈

简介&#xff1a;本资源是一套基于OPNET平台实现DSR&#xff08;动态源路由&#xff09;协议的完整仿真源码&#xff0c;面向网络协议学习者、无线Ad Hoc网络研究者及通信工程高年级本科生与研究生&#xff0c;用于深入理解DSR在移动自组织网络中的路由发现、路径维护与MAC层协…

作者头像 李华