回答简单描述

用户输入esc之后是如何终止任务的?

  • 键盘输入的内容可以被捕捉,然后判断这个ESC是否是孤立的,触发就取消
  • (可以作为项目难点:我输入 ESC 的时候不能随时响应)Agent执行任务可能需要很长时间,如果在主线程执行取消任务,用户按 ESC 主线程任务占用,无法响应。所以解决方案就是让主线程一直空闲,一直保持着能和我的命令交互,然后开启后台线程,让所有和LLM交互的任务都通过后台线程执行。把任务提交到后台,开始监听ESC,如果出现就直接中断后台任务。并且每 150ms 检测任务是否完成。
  • 这里会使用一个函数式接口Callable runTask;然后把这个任务传给一个方法,创建后台线程并执行这些问题。而且这个后台线程是守护线程,这个可以跟着主线线程结束而结束。
  • 这里加分点多加了一个任务在线程池多线程并行执行问题
  • 并行工具有场景冲突问题,比如同时读写同一个文件

短期记忆和conversionHistory的设计对比

  • LLM 和我交互中有三个JSON结构, model模型,message对话消息,tools 我注册的工具:工具里面要包括:type类型,function:里面又包括名字name ,description描述,调用这个函数需要的参数。
  • 每一次 conversionHistory 的内容就相当于是 message对话消息,包括 role:用户类型,content:对话内容,tool_calls要调用的工具(包括有LLM生成的唯一标识 ID,type类型, 参数JSON)
  • 计算token预算,大概 1.5 个汉字一个 token ,其他字符没4个字符一个 token。如果超过 token 阈值, 保存最近3轮对话,前面的都组合成新的 List 发送给LLM ,进行摘要总结,每一条信息都是一个 Message实体。
  • 短期记忆就让 conversationHistory 充当,这个在每次LLM 调用前都会检测是否达到token阈值,如果达到阈值进行压缩,有两种策略,第一种之和LLM交互一次,把最近3条之前的所有内容直接压缩成摘要,这样速度快,但是可能遗漏信息,第二种每5条进行一次压缩,最后进行一次合并,但是这样消耗token就比较多,而且很慢。还要注意切割点一定要以这个 user message ,也就是这个用户说话结束,下一个用户说话之前阶段,这样避免中间把 tool_call_id 和 工具调用返回结果分开,这样发给LLM ,有可能检测不到是哪个工具调用的结果,产生报错或者混乱
  • 长期记忆存储: 第一个可以和LLM 交互判别是事实类型然后加入长期记忆,第二种是 /save 手动命令添加,第三种在压缩的时候把压缩摘要也加入到长期记忆。删除主要是根据 id 删除的,可以先通过 list 列出所有的长期记忆,然后再使用clear 进行删除
  • 关于这个存储文件,我本来想的是在每一个项目下面都维护一个memory文件,在这个项目的长期记忆都存储到这个文件中,但是我发现一个问题就是如果有一些想通的特点,比如我主语言就是 Java,但是只在某一个项目的文件里面写入,切换到其他的项目里面又会不知道,所以后来我就想把所有长期记忆存到一个文件里,这个路径就固定到C盘用户目录下面,然后每一个记忆实体多加两个参数,一个就是项目路径,我在存入这个记忆的时候会把这个记忆和项目路径进行绑定,第二个参数就是设置为通用类型,这样我在项目启动读取长期记忆的时候,就可以选择性读取,只读和我这个项目路径一致的以及通用的长期记忆。
  • 长期记忆是要写入磁盘的,每次项目启动的时候都要去读取磁盘,文件路径第一优先从 JVM参数中读取,第二 .evn环境变量中
  • 长期记忆查询匹配使用的是 jieba 进行中文分词,保留关键词,比如【JDK,版本】,然后遍历进行关键词匹配。double timeDecay = Math.max(0.5, 1.0 - ageHours / 24.0);时间衰减,从最开始的1.0权重最高,到24小时之后衰减到0.5,长期记忆不是特别多,使用关键词记忆就够了
    image

image

MCP

  • 使用 JSON-RPC协议,两种模式 stdio 和 streamable Http 模式,无论哪种每次开始之前第一件事就是初始化握手,客户端发送 initialize ,告诉服务端我是谁,支持什么能力,我用什么协议版本,客户端回复自己的信息和能力,握手成功之后客户端再发一条 notifactions 通知表示我准备好了可以开始干活了。两次握手轻量级,如果有问题相比于调用 tools 耗费的时间更少
  • JSON-RPC 的三种消息类型:Request(协议版本,id, 方法名字(比如 tools/list 请求获取所有工具),参数(方法名字,参数)),响应(协议版本,id, 结果(内容)暂时只是文本内容),通知,这个通知有三种类型,tools/list_changed 工具列表变了、resources/list_changed 资源列表变了、resources/updated 某个资源的内容更新了,这是 MCP 协议标准定义的。
  • 握手完成之后客户端第一件事就是问服务端你有哪些工具,然后服务端返回一个list工具列表,包含工具名称,参数,拿到工具列表之后会把每个工具进行注册
  • 项目有一个配置文件 mcp.json ,会自动创建在我本机电脑C盘的user文件里面
  • 简单说,Tools 是能干活的,有输入参数、会改变状态;Resources 是能读的,通过 URI 访问,返回内容,只读不写。你可以把 Tools 理解成 POST 端点,Resources 理解成 GET 端点。但实际使用中有个问题:LLM 不能直接调 resources/read,因为 Function Calling 协议里只有 tools 的概念,没有 resources。
    • 第一个把这个资源也包装成一个工具,LLM 调用这个工具,通过 url 从 MCP 直接读取资源内容,返回文件内容
    • 第二个用户显示知道要调用哪个文件,直接先去调用 mcp server 获得资源,然后再一起返回给 LLM
    • 但是读取文件内容如果太多我要进行截断,现在是截断开头和结尾,只保留这两个部分在,并且会返回这个文件的大小,然后添加了一个命令 cat 可以查看整个文件的内容。
  • 完整步骤:
    • 先去 mcp.json 读取配置,直到我可以调用哪些工具,比如本地系统工具,远程数据库工具
    • 区分使用哪种传输方式,如果配置中有 command字段使用 stdio ,如果有url字段使用 HTTP传输
    • 创建多个线程,每一个 MCP Server 都是独立外部服务,不能因为这个请求阻塞线程,就像同时点了五道菜可以让厨师同时做5道菜。创建了线程池,可以同时启动多个mcp server,初始化建立连接,并且发送 list获取所有的工具完成工具的注册,然后就结束这个线程池。
    • 除此之外,在将进行 stdio 传输的时候:我们还额外开启了两个独立的子线程处于阻塞状态,其中一个stdout线程比如要调用某个工具,我的主线程发出了请求,等这个server返回响应,就可以让这个线程去处理,主线程可以去做别的事情,比如我需要调用3个工具,这时候主线程就可以直接去进行其他请求的发送不用在这里等待,还有比如我发出 ESC 命令,如果主线程被阻塞在这里就没办法接收到了,程序无法结束。还有这里如果 stdout 读取到通知消息,还需要再去单独开一个线程发送请求,避免发送请求占据了这个 stdout 读取线程,导致死锁。另外一个独立线程需要去读取日志。
    • 而在 HTTP 传输中就不需要单独再开两个线程了,因为可以使用 OKHTTP 内部有线程池,后台线程发送请求之后就可以去处理其他的请求任务了
    • 建立初始连接,发送 JSON-RPC 请求,接收 JSON-RPC响应,初始化成功之后要调用 list 请求,获取该 server 提供的所有工具,并且进行重新命名
  • 本地工具 stdio : list_files:列出指定目录下的文件,read_file:读取本地文件内容,write_file:写入 / 覆盖本地文件
  • http 远程工具:get_weather:查询城市实时天气(调用公共天气 API),查询某种股票的价格
  • mcp 和 Function calling:Function Calling 是 LLM API 层的协议,干两件事:告诉 LLM"你有哪些工具能用",以及让 LLM 说"我要调这个工具"。MCP 是工具提供方的协议,解决的是"工具从哪来、长什么样、怎么执行"。
  • mcp 的 schema 清洗: MCP Server 返回的工具参数是标准 JSON Schema,但 LLM 不是 JSON Schema 解析器,有些复杂结构它处理不好,有 $ref,oneof二选一这种字符,所以需要进行一个 schema 清洗

提示词和Skill

  • 系统最基本的system prompt要包含:第一身份,我这个项目身份就是面向代码库的工作的智能 Agent,第二个,行为规范,比如回答的时候使用中文,代码,文件名,命令都必须要保持原文,第三个工具的调用,告诉LLM 我有哪些工具,每个工具有什么功能,第四个安全策略,比如禁止一些全盘删除命令
  • 提示词分成多个 .md 文件,进行模块化划分有工作模式选择(默认 react模式, Plan-and-Execute模式),动态注入部分(Skill索引,记忆外部上下文),有一个基本 base.md 用于一些通用的规则prompt,然后进行不同模块的提示词进行拼贴在一起,不变的提示词在前面,动态的提示词在后面
  • skill就相当于一个专业手册,Agent好像什么都会一点,但是遇到专业问题的时候不够深入,于是给他准备了基本专业手册,比如《网页访问手册》如何高效抓取网页,处理登录,《数据库优化手册》设计索引,查询优化的最佳实践
  • skill 索引段轻量级放在 system prompt 里面,懒加载,只有当LLM需要的主动调用的时候才加载。Agent 启动时,只把所有启用 Skill 的 name + description 渲染成一段索引,注入到 system prompt 末尾,整个索引控制在 4KB 以内。LLM 看到的相当于一份菜单,而不是所有 Skill 的完整内容。运行时,LLM 根据用户输入判断需要哪个 Skill,主动调用 load_skill(name) 工具。加载后 Skill 的正文会写入一个缓冲区,在下一轮对话时前置注入到 user message 前面。注入是一次性的,取出后自动清空,不会跨轮重复注入。
posted @ 2026-06-03 21:18  Huangyien  阅读(4)  评论(0)    收藏  举报