在这里插入图片描述
P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

1. 咱们今天要做个啥

1.1 先搞懂Mini Harness是啥

不知道你们写Agent框架有没有过这种噩梦。

上来先整个主文件,吭哧吭哧写模型调用,写完加工具,加完写循环,加着加着文件就奔三千行去了。

后来想加个日志功能,得改循环代码;想换个模型,得翻半天才找到调用的地方;想临时禁用个工具,注释完还得补一堆判断,生怕哪漏了程序直接炸。

说白了就是典型的“屎山式开发”,所有功能揉在一块,牵一发而动全身,改个需求跟拆炸弹似的。

今天咱们就换个玩法,用Cordis从零拼一个迷你版的DeepSeek Harness,全程插件化,想加啥插啥,想删啥拔啥,主程序?不存在的。

简单说,这就是一个能跑起来的最小Agent框架。

能调用DeepSeek的模型,能执行shell命令,能发HTTP请求,能搜本地代码,完整跑通Agent思考-调用工具-再思考的循环。

麻雀虽小五脏俱全,核心机制和官方的DeepSeek Harness一模一样,因为咱们用的就是同一个底座Cordis。

最有意思的是,这玩意没有所谓的“主程序”。

所有能力都是一枚枚独立的插件,你把需要的插件拼到一起,框架就成了。就像拼乐高,你要跑车就拼跑车,要城堡就拼城堡,零件都是通用的。

今天咱们一共拼八枚插件,三个核心服务,两个事件,最后串起一条Agent循环。

1.2 前置准备

先说好门槛,其实很低。

Node.js版本18以上就行,再去DeepSeek官网申请个API Key,配到环境变量里。

别装错包,npm上的包名就叫cordis,那个@cordisjs/core是老版本遗留的,别下错了,不然跑不起来别来找我。

运行TypeScript用tsx,因为Cordis的模块是无扩展名导入的,原生Node跑不了,这个坑我替你们踩过了。

2. 第一枚插件:从Hello World开始

2.1 最小的插件长啥样

先整个最简单的热热身,让你们看看Cordis的插件到底是个啥。

代码特别短,导入Context,new一个实例,然后调用plugin方法传个对象进去,有name和apply两个字段就行。

import { Context } from 'cordis'
const app = new Context()
app.plugin({
  name: 'hello',
  apply(ctx: Context) {
    console.log('hello from my first plugin')
  },
})

跑起来控制台就会打一行日志,恭喜你,写出了人生第一个Cordis插件。

说穿了很简单,一个Cordis应用就是一个上下文Context。你创建根上下文,然后把插件一个个挂上去。

插件的apply方法就是入口,框架加载的时候自动调用,把上下文交到你手里,你想干啥干啥。

2.2 插件的三种形态

插件不是只有一种写法,一共有三种形态,运行起来效果都一样。

最简单的就是函数形式,直接写逻辑;刚才说的是对象形式,能带名字带配置;还有类的形式,就是后面要讲的Service服务。

选哪种全看需求,要啥自行车就配啥零件,不用硬套格式。

3. 服务:把能力挂上上下文

3.1 为啥要有服务这东西

刚才的Hello World插件,只能证明它能被加载,啥用没有。

真做项目,插件和插件之间得互相调用对吧?总不能每个插件都自己实现一遍模型调用吧,那不成重复造轮子大赛了。

有人说,那我import不就完了?

问题就在这,一import就耦合死了。今天你用这个LLM实现,明天想换个实现,所有import的地方全得改,跟把房子承重墙抽了换根新的似的,费劲不说,还容易塌。

Cordis的解法叫服务。

你把能力做成服务,挂到上下文ctx上,其他插件按名字找服务就行,不用管你具体是怎么实现的。就像小区里的快递柜,你知道它能取件寄件就行,不用管里面是什么结构、哪个公司做的。

3.2 手写一个LLM服务

咱们直接上手写第一个服务,就是模型调用的LLM服务。

先做类型声明,告诉TypeScript,ctx上面以后多了个llm属性,以后在哪拿Context都能直接点出llm,类型还不报错,这就是按名字找服务的底气。

然后写个LlmService类,继承Service,构造函数里调用super注册服务名。配置项都给好默认值,模型默认用deepseek-v4-flash,够快够便宜,日常用完全够。

export class LlmService extends Service {
  config: Required<LlmConfig>
  constructor(ctx: Context, config: LlmConfig = {}) {
    super(ctx, 'llm')
    this.config = {
      model: config.model ?? 'deepseek-v4-flash',
      baseUrl: config.baseUrl ?? 'https://api.deepseek.com',
    }
  }
  async chat(messages: ChatMessage[]): Promise<string> {
    // 标准OpenAI兼容请求
  }
}

核心就是一个chat方法,发请求调DeepSeek的API,标准的OpenAI兼容格式,拿到结果返回纯文本。

现在这个服务就挂到ctx上了,以后哪个插件要调用模型,直接ctx.llm.chat就行,不用管你是调DeepSeek还是调别的,接口统一就行。

4. 工具注册表:先搭台子再唱戏

4.1 注册表是干啥的

Agent要干活,得有工具对吧?总不能光靠嘴说。

很多人写工具,直接写在Agent代码里,加一个工具就得改Agent的代码,加十个工具Agent代码就胖一圈,最后Agent啥都干,就是不像个Agent。

咱们不这么干。先整个工具注册表服务,专门管工具的登记、描述、执行,它自己一个具体工具都不提供。

就像商场的招商部,只管租摊位、做导览,自己不卖货。店铺是各个商家自己来开,想开什么店开什么店,不想开了就走,商场本身不用动。

4.2 工具协议长啥样

要注册工具,得先定规矩,也就是工具协议。

每个工具必须有名字、描述、参数说明,还有一个执行函数。描述和参数是写给模型看的,得说清楚这个工具能干啥、传什么参数,不然模型不会用。

注册表就三个方法:register登记工具,describe把所有工具拼成一段文字给模型看,execute按名字执行工具。

就这么简单,剩下的具体工具,咱们用插件来做。

5. 三个真实工具:给Agent装上手脚

5.1 bash工具:让模型能敲命令

第一个工具,bash执行,最实用也最危险的工具。

就写一枚叫mini-bash的插件,声明自己依赖tools服务,框架自动等tools就绪了再加载它,启动顺序完全不用你管。

以前写代码,你得自己安排加载顺序,先加载注册表,再加载工具,写反了直接报错,就像做饭先炒菜再买米,纯纯离谱。现在有了依赖注入,你只要说你需要啥,框架给你安排得明明白白。

export const inject = ['tools']
export function apply(ctx: Context) {
  ctx.tools.register({
    name: 'bash',
    description: '执行一条 shell 命令并返回输出',
    parameters: { /* 参数说明 */ },
    async execute(args) {
      // 执行命令,10秒超时
    },
  })
}

实现也简单,用node的child_process执行命令,包一层promise,设个10秒超时,别让死命令把程序挂死。

重点说一句,工具执行出错别往外抛异常,把错误信息当成字符串返回给模型。

模型很聪明的,知道出错了会自己调整策略,你一抛异常,整个Agent循环直接崩了,就像人吃饭咬着沙子直接把碗砸了,没必要。

5.2 fetch工具:让模型能上网

第二个工具,HTTP请求,也就是fetch。

模板和bash一模一样,也是插件,也依赖tools服务,注册一个叫fetch的工具。

支持自定义请求方法和请求体,返回状态码和文本内容,太长的话截断一下,别一下塞太多内容给模型,它也消化不了。

有了这个工具,模型就能自己查资料、抓接口,不用你提前把信息喂给它了。

5.3 文件搜索工具:让模型能翻代码

第三个工具,本地文件搜索,写代码的Agent必备。

功能很简单,给个关键词,递归搜目录里的文本文件,跳过node_modules和隐藏目录,深度别太深,不然搜起来没完。

Agent写代码的经典流程就是:先搜关键词定位文件,再读文件内容,最后修改。这第一步就靠这个工具。

三个工具装完,模型就有手有脚了,能跑命令、能上网、能翻代码,基础能力齐活。

以后想加新工具?照着模板再写个插件就行,一行原有代码都不用改。想禁用哪个工具?从装配清单里删掉那一行就行,干净利落。

6. 提示词居然也是插件

6.1 提示词写死的坑

我见过太多项目,提示词直接硬编码在Agent代码里,一大坨字符串,看着都头疼。

今天要加个安全规则,明天要加工具描述,后天要加角色设定,改来改去,提示词比业务逻辑还长,改的时候生怕删错一个标点,跟改宪法似的。

更坑的是,多个模块都想改提示词的时候,总不能都去改同一段字符串吧,最后肯定改乱套。

咱们的玩法是:提示词也做成插件。

6.2 用事件链拼提示词

这里要用到Cordis的事件,准确说是waterfall瀑布流事件。

啥意思呢?就是一个事件发出去,监听器一个接一个处理,上一个的输出是下一个的输入,最后返回最终结果。就像流水线,第一个人装外壳,第二个人装零件,第三个人打包,最后出来成品。

export function apply(ctx: Context) {
  ctx.on('prompt/build', async (base, next) => {
    const downstream = await next()
    return `${downstream}\n\n可用工具:\n${ctx.tools.describe()}\n\n${RULES}`
  })
}

咱们搞一个prompt/build事件,Agent那边发出事件,传一个基础提示词。然后提示词插件监听这个事件,在后面追加上工具清单和回答规则,再返回去。

以后想再加一段提示词?再写个插件监听同一个事件就行,不用动现有代码。想加安全规则就加安全插件,想加角色设定就加角色插件,各管各的,互不干扰。

提示词从一段写死的字符串,变成了一条可插拔的处理链,想加啥加啥,想删啥删啥。

7. Agent循环:把大脑和身体连起来

7.1 Agent循环到底在转啥

前面的零件都备齐了,现在就差个核心大脑,也就是Agent循环。

说穿了Agent循环就是个复读机:调模型拿回复,判断是要调用工具还是给最终答案,调用工具就把结果填回去再调模型,直到出答案或者转够圈数。

听起来简单,真写起来坑不少。

7.2 核心代码拆解

咱们把Agent也做成一个服务,挂到ctx上,依赖llm和tools两个服务。

首先是构建提示词,不用自己拼,直接发waterfall事件,拿到最终结果就行,它根本不知道有多少插件改了提示词。

然后是run方法,也就是主循环,默认最多转5圈,防止模型上头了一直调用工具停不下来,这是安全阀,必须有。

每一轮调模型,然后解析返回的JSON。有answer就直接返回结果,有tool就执行工具,把结果当成用户消息塞回去,继续下一轮。

解析失败也别慌,直接把原文返回给用户就行。模型偶尔不按套路出牌太正常了,你跟它较真你就输了,兜底逻辑一定要做好。

7.3 事件的妙用

重点说一下,每一步操作咱们都发一个agent/step事件出去。

Agent自己其实用不上这个事件,那发它干啥?

给别人用啊。比如你要加日志,要做统计,要加敏感信息过滤,都可以监听这个事件,不用改Agent半行代码。

这就是插件化的精髓:核心逻辑只管自己的事,周边能力全靠事件扩展。就像手机本身只有基础功能,你要拍照、要支付、要刷视频,装APP就行,不用改手机系统。

8. 组装起来跑一跑

8.1 八枚插件拼全家桶

零件都齐了,现在开始组装。

创建一个根上下文,然后把插件一个个挂上去:llm、tools、三个工具插件、prompt、agent,再加个日志插件tracer,正好八枚。

const app = new Context()
await app.plugin(llm)
await app.plugin(tools)
await app.plugin(bash)
await app.plugin(fetchTool)
await app.plugin(search)
await app.plugin(prompt)
await app.plugin(agent)
await app.plugin(tracer)

这个tracer插件就是个现成的例子,它监听agent/step事件,每次工具调用都打一行日志。它不认识Agent,Agent也不认识它,俩插件唯一的交集就是那个事件。

你把tracer删掉,Agent照样跑;你把tracer换成写文件、发通知的插件,Agent也啥都不知道。

这就是解耦的快乐。

8.2 实际跑起来看看

整个交互界面就用readline整个简单的命令行,输入问题就能跑。

比如你问它“当前目录哪些文件提到了waterfall”,它会先调用文件搜索找到对应文件,再用bash命令定位行号,最后给你一个准确答案。

全程自动的,你就看着它一步步调用工具就行,还挺有意思的。

控制台里那些[tool]开头的日志,就是tracer插件打出来的,活脱脱的事件机制现场演示。

9. 最后唠两句实在的

9.1 Cordis到底像啥

很多人问,Cordis这插件机制,是不是跟Express中间件差不多?

像,但又不一样。

Express的中间件是请求级的,一次请求穿过去就没了。Cordis的插件是应用生命周期级的,启动加载,卸载清理,贯穿整个应用的一生。

真正像Express中间件的,是waterfall事件链,每次触发都走一遍处理链,这才是请求级的体验。

说白了就是两层中间件:插件是应用生命周期的中间件,事件链是每次分发的中间件。搞懂这两层,Cordis就算玩明白了。

9.2 和真实Harness的差距

实话实说,咱们这个迷你版,和官方的DeepSeek Harness比,差的东西还不少。

没有会话记忆,每次对话都是全新的;没有流式输出,都是等模型说完才返回;用的是文本JSON协议,不如原生function calling稳;还有配置校验、安全治理这些功能都没有。

但骨架是一模一样的:能力拆插件,服务做协作,事件做扩展。

搞懂这个迷你版的原理,再去看官方Harness的源码,就不会觉得是天书了,无非就是插件更多、功能更全,核心逻辑没变。

想深入研究的,自己去翻官方文档和源码,今天就先唠到这。

P.S. 目前国内还是很缺AI人才的,希望更多人能真正加入到AI行业,共同促进行业进步,增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow,教程通俗易懂,高中生都能看懂,还有各种段子风趣幽默,从深度学习基础原理到各领域实战应用都有讲解,我22年的AI积累全在里面了。注意,教程仅限真正想入门AI的朋友,否则看看零散的博文就够了。

Logo

更多推荐