ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用D语言构建本地LLM Agent:自省机制让AI认识自己

用D语言构建本地LLM Agent:自省机制让AI认识自己 这次我们来看一个不太常见但很有意思的组合用D 语言DLang写一个本地LLM Agent。项目和标题直译过来就是“教 AI 认识自己”做法不是让模型背一段自我介绍而是给 agent 装上一套能真实读取自身状态、工具列表和运行时配置的“自省机制”。它说出来的“自己是谁”来自程序在内存里查询出来的真实数据而不是提示词里编好的话术。为什么选 D 语言因为 D 编译出来是原生可执行文件启动速度比 Python 脚本快一个数量级内存占用可控二进制分发也简单。LLM Agent 的本质就是“对话循环 上下文管理 工具调用”这些逻辑用 D 写并不会比 Python 复杂太多但部署形态会干净很多。这篇文章会带你走完整条路径D 编译环境准备、接入本地模型、实现多轮对话、工具调用、自省机制、HTTP 接口和批量任务。先说结论这个方案真正的价值在于“本地、可控、可解释”。所有请求都在 127.0.0.1 回环内完成模型可以换成任意尺寸的小参数量化模型3B/7B 级别在消费级显卡或纯 CPU 环境中都能尝试。agent 的每一步决策都会打印日志方便排查。所以它适合两类人一类是 D 语言开发者想在 AI 生态里找用武之地另一类是 LLM 应用开发者觉得 Python agent 框架太重想找一个更朴素、更能看清底层的实现。从标题的设计思路看这个项目的重点是“教 AI 认识自己”也就是给模型一套工具让它能主动查询自己的配置和运行环境。这篇文章会围绕这个核心展开不依赖任何现成的重量级 agent 框架直接把最小可运行的架构写给你看。1. 核心能力速览能力项说明项目定位本地 LLM Agent 架构演示D 语言实现主程序模型接入方式通过 HTTP API 调用本机模型服务Ollama / llama.cpp server / LM Studio 等核心功能多轮对话、上下文管理、工具调用、运行时自省运行形态D 代码编译为单个原生可执行文件启动快、分发简单硬件门槛取决于本地模型规模小参数量化模型可在消费级 GPU 或纯 CPU 环境测试启动方式先启动模型服务再运行 agent 可执行文件接口能力可额外通过 vibe.d 暴露 HTTP API供其他程序调用批量任务支持从文件读取多条指令循环调用模型并输出结果适合人群D 语言开发者、本地 AI 工具爱好者、内网 agent 服务开发者这里要特别说明显存占用与模型参数量、量化方式、上下文长度直接相关不存在一个固定的数字。以 7B 量化模型为例常见做法是优先考虑 4-bit/8-bit 量化版本来压缩显存具体数值需要在本机单独验证。2. 这个项目解决什么问题2.1 “认识自己”是什么意思普通 LLM 应用只会把用户问题抛给模型然后展示回答。这个项目做了一件更内省的事让 agent 有能力回答“你能做什么”“你当前用了什么模型”“你的上下文还剩多少”“你现在有哪些工具”这类问题。回答的依据不是模型幻觉而是 D 程序在调用工具时动态读取的真实状态。这样做有一个实际好处agent 的能力边界不再靠文档口头描述而是可以被模型主动感知。比如用户问“你能读取本地文件吗”agent 会先调用llm_config工具发现工具列表里有file_read再回答“可以”。模型是在“确认过自己确实有这个能力”之后才给出结论而不是猜的。2.2 D 语言在 LLM agent 里的定位D 语言经常被低估但它在系统编程领域有独特的生态位置原生编译没有解释器启动开销适合做常驻后台服务。与 C ABI 高度兼容可以直接extern(C)链接 llama.cpp 等 C 库。自带 GC但不强制局部性能敏感路径可以手动管理内存。dub包管理工具成熟依赖声明简单二进制发布容易。LLM Agent 的主程序本身并不需要跑在 GPU 上它只负责拼 prompt、调接口、解析响应、执行工具。这部分逻辑是 IO 密集逻辑密集D 语言的性能优势在这里体现得很明显。如果只是做一个内部 CLI agent一个 3MB 左右的静态可执行文件比一个几百 MB 的 Python 虚拟环境干净太多了。2.3 与 Python agent 框架的对比Python 生态有 LangChain、LlamaIndex 这些成熟框架但问题是抽象层太多Chain、Agent、Tool、Memory、Callback 一层套一层出了问题很难定位。而 D 语言没有那么多现成的 agent 框架你反而会被迫把整个链路写清楚怎么组织 messages、怎么裁剪上下文、怎么解析工具调用、怎么处理失败。这种“没有框架束缚”的状态特别适合用来学习 agent 的核心机制。3. 适用场景与使用边界3.1 适合做什么内网工具集成把本地模型封装成一个 HTTP 服务供后端系统调用。命令行 agent在终端里与本地模型对话快速执行系统命令、读取文件等。批量文本处理把待处理文本逐条喂给模型输出结构化结果。agent 教学与原理验证用最少的依赖看懂“对话循环 工具调用”是怎么跑的。策略性自省工具让 agent 在回答前先确认自身能力减少幻觉式回答。3.2 不适合做什么不适合复杂的 agent 工作流编排比如多 agent 协作、图状任务流。不适合需要调用大量 Python ML 生态如 numpy、torch、transformers的场景。不适合快速原型验证D 的编译-运行循环比 Python REPL 慢。不适合直接拿来做高并发生产 API除非你对模型吞吐和并发控制做了充分压测。3.3 合规与安全边界本地部署不等于没有合规问题模型权重来自公开渠道时注意确认模型的 license 是否允许商用和二次分发。如果 agent 被赋予“可执行系统命令”“可读取本地文件”的能力必须限定访问目录和命令白名单绝对不要让 agent 以 root 或管理员权限运行。使用工具调用返回的数据时要防止 prompt injection模型可能在解析内容时被诱导输出恶意指令工具层必须对模型返回的“指令”做格式校验不能盲目执行。任何涉及导入、导出、生成内容的场景都要确认数据来源合法、输出内容不侵犯他人版权和隐私。4. 环境准备与前置条件4.1 D 语言编译环境D 语言有三种主流编译器dmd、ldc、gdc。建议使用dmd官方参考编译器或者ldc基于 LLVM生成代码性能更好。Windows 可以直接从 D 语言官网下载安装包也可以使用包管理器安装。Linux 下常见方式# 以 Ubuntu/Debian 为例命令以官方文档为准 wget https://downloads.dlang.org/releases/2.x/2.109.1/dmd_2.109.1-0_amd64.deb sudo dpkg -i dmd_2.109.1-0_amd64.debmacOS 可以使用 Homebrewbrew install dmd安装完成后验证版本dmd --version dub --version只要能输出版本号说明编译器和包管理器都可用。4.2 本地模型服务D 程序本身不直接加载模型而是通过 HTTP API 调用本机模型服务。推荐以下三种之一服务特点默认端口Ollama安装简单模型下载一条命令生态完善11434llama.cpp server单二进制文件可跑 GGUF 量化模型CPU/GPU 都支持8080LM Studio图形化管理模型内置本地 API Server1234以 Ollama 为例安装后先拉取一个小参数模型# 拉取一个小模型用于验证具体模型标签以 Ollama 库为准 ollama pull qwen2.5:3b如果你的机器没有 NVIDIA GPU也可以选择 CPU 推理。可以用更小的模型继续验证# 更小的模型CPU 也能跑 ollama pull qwen2.5:0.5b拉取完成后启动服务ollama serve用下面的命令确认服务可用curl http://127.0.0.1:11434/api/tags如果能返回一个 JSON 对象列表模型服务就绪。4.3 磁盘与端口检查模型文件通常占用几 GB 到十几 GB 空间安装前检查磁盘剩余空间。Windows 用户如果 C 盘空间紧张可以把模型目录放到 D 盘。默认端口被占用时需要修改模型服务配置或使用--port参数指定新端口D 程序里的 URL 也要同步修改。5. 架构与自省设计5.1 总体数据流这个项目分为三层模型服务层负责真正的大模型推理由 Ollama 或 llama.cpp server 提供。Agent 逻辑层D 语言编写负责组装 messages、调用模型接口、解析响应、裁剪上下文。工具层D 程序内置的工具函数模型通过统一协议触发。完整的请求链路是用户输入 - D agent 主循环组织 messages - POST http://127.0.0.1:11434/api/chat - 模型返回文本 - D agent 判断是否包含 TOOL 指令 - 执行工具把结果写回上下文 - 再次调用模型得到最终回复 - 输出给用户5.2 系统提示词让模型知道自己是谁实现“认识自己”的关键在系统提示词设计。我们把 agent 的能力清单、工具协议、运行环境全部写进系统提示词你是一个运行在本地 D 语言程序中的 LLM Agent。 每当用户询问你的能力、状态、配置或工作方式时 你必须先调用 llm_config 工具获取真实信息再基于工具返回值回答 不要凭记忆猜测。 你拥有的工具如下 - llm_config: 查询当前模型名称、上下文历史长度、可用工具列表、宿主系统架构。 - system_info: 查询本机操作系统、CPU、内存等信息。 - file_read: 读取指定文本文件内容仅限工作目录下。 工具调用格式 TOOL:工具名|JSON参数 例如 TOOL:llm_config|{} 工具执行后系统会以 rolesystem 返回结果。这套设计保证了模型对“自己是谁”的回答建立在真实状态上。5.3 工具协议TOOL 单行协议为了让模型输出可以被稳定解析我们规定一个简单的文本协议模型需要调用工具时输出一行以TOOL:开头的内容D 程序检测到这一行就拆分工具名和 JSON 参数执行对应的 D 函数把结果作为一条system消息追加到上下文然后继续让模型生成最终回复。这个协议不依赖模型的 function calling 能力兼容性更好。如果你的模型支持原生 function calling也可以替换成 structured JSON 输出原理一致。5.4 上下文管理LLM 接口通常有上下文长度限制多轮对话后必须裁剪。最简单的策略是保留系统提示词。保留最近的 N 轮对话。超出窗口时丢弃最早的轮次。6. 代码实现D 语言 Agent 主程序下面给出一个教学用简化实现。重点看结构依赖版本和接口细节需要按你的 dub 环境调整。6.1 dub.json{ name: local-llm-agent, dependencies: { requests: ~2.1.2 }, targetType: executable }requests是 D 语言的 HTTP 客户端库。如果后续要加 HTTP 服务再增加vibe-d依赖。6.2 模型客户端先写一个最核心的函数向 Ollama 的/api/chat发送 messages返回模型回复。import std.string; import std.json; import std.net.curl; import std.stdio; string chatWithModel(string[] messages) { string model qwen2.5:3b; string body { model: ~ model ~ , stream: false, messages: [ ~ messages.join(,) ~ ] }; auto http HTTP(http://127.0.0.1:11434/api/chat); http.method HTTP.Method.post; http.headers [Content-Type: application/json]; auto response http.post(body); auto data parseJSONString(response.body); return data[message][content].str; }这里的messages是已经序列化好的 JSON 数组片段例如{role:user,content:你好}。生产环境建议用std.json统一构造和解析避免字符串拼接注入问题。6.3 工具与自省实现llm_config是这个项目的重点工具它负责返回 agent 的真实状态import std.process; string callTool(string toolName, string argsJson) { switch (toolName) { case llm_config: JSONValue cfg; cfg[model] qwen2.5:3b; cfg[history_length] currentHistory.length; cfg[max_history_length] 20; cfg[available_tools] [llm_config, system_info, file_read]; cfg[host_arch] size_t.sizeof * 8; return cfg.toPrettyString(); case system_info: auto result executeShell(uname -a); return result.output; case file_read: return readFile(argsJson); default: return unknown tool: ~ toolName; } } string readFile(string path) { import std.file : exists, readText; if (!path.exists) return file not found; auto content readText(path); if (content.length 500) content content[0 .. 500]; return content; }注意这里的currentHistory是全局变量实际代码需要定义。工具返回值要限制长度防止撑爆上下文。6.4 Agent 主循环主循环的核心逻辑是检测TOOL:前缀并执行工具void main() { writeln(Local LLM Agent (D Language)); writeln(输入 exit 退出); string[] history; while (true) { write( ); auto input readln().strip; if (input.empty) continue; if (input exit) break; history ~ {role:user,content: ~ input ~ }; writeln(... calling model ...); auto reply chatWithModel(history); if (reply.startsWith(TOOL:)) { auto line reply[5 .. $].strip; auto parts line.split(|); auto toolResult callTool(parts[0].strip, parts[1].strip); history ~ {role:tool,content: ~ toolResult ~ }; reply chatWithModel(history); } writeln(Agent: , reply); history ~ {role:assistant,content: ~ reply ~ }; // 上下文裁剪保留最近 20 条消息 if (history.length 20) history history[$ - 20 .. $]; } }这个循环就是 agent 的最小骨架用户输入、组装消息、调用模型、检测工具调用、执行工具、再次调用模型、输出回复、裁剪历史。6.5 编译与启动在项目根目录执行dub build编译成功后确保模型服务先启动ollama serve然后运行 agentdub run # 或者直接运行编译产物 ./local-llm-agent进入交互界面后输入任意文字测试。7. 功能测试与效果验证7.1 模型服务连通性测试目的确认 D 程序能访问到模型服务。curl http://127.0.0.1:11434/api/tags预期返回包含模型列表的 JSON。如果不是这样先检查 Ollama 进程和端口。7.2 单轮对话测试在 agent 中输入 你好请用一句话介绍你自己预期模型返回一段自我介绍。如果返回内容中包含“运行在 D 语言程序中的 agent”说明系统提示词生效。7.3 自省能力测试这是这个项目最关键的验证点。输入 你现在有哪些工具请列出并说明用途预期模型不会直接回答而是先输出TOOL:llm_config|{}D 程序执行工具后再生成最终回答。最终答案里应该出现llm_config、system_info、file_read和对应的说明。判断标准如果最终答案里出现了工具名但工具名与llm_config返回的available_tools列表一致说明自省链路通了。7.4 上下文裁剪测试连续对话超过 20 条消息后输入 你还记得我们最开始聊了什么吗预期如果最开始的对话已经被裁剪模型会表示不记得。这是正常现象说明上下文管理机制在工作。7.5 失败场景复现不启动 Ollama直接运行 agent输入任意内容。预期程序会抛出连接异常或请求超时。此时需要看日志确认异常类型。8. 接口 API 与批量任务agent 做好了接下来把它封装成服务接入其他系统。8.1 使用 vibe.d 暴露 HTTP 接口在 dub.json 中增加vibe-d依赖{ name: local-llm-agent, dependencies: { requests: ~2.1.2, vibe-d: ~0.9.6 } }创建一个 HTTP 服务入口import vibe.d; void handleChat(HTTPServerRequest req, HTTPServerResponse res) { auto body req.json; auto prompt body[prompt].str; auto reply chatWithModel([{role:user,content: ~ prompt ~ }]); res.writeJson(JSONValue([reply: reply])); } void main() { auto settings new HTTPServerSettings; settings.port 8080; listenHTTP(settings, handleChat); runEventLoop(); }这里只写了一个极简端点实际使用还需要补参数校验、超时控制、并发限制。8.2 curl 调用示例服务启动后用 curl 验证curl -X POST http://127.0.0.1:8080/chat \ -H Content-Type: application/json \ -d {prompt: 请列出你的可用工具}预期返回{ reply: TOOL:llm_config|{}\n我当前可用的工具包括llm_config、system_info、file_read。 }8.3 批量任务如果需要批量处理文本可以直接在 D 程序里写一个批量入口从文件逐行读取指令并输出结果import std.file : readText, write; import std.string : splitLines, strip; void runBatch(string inputFile, string outputFile) { auto lines readText(inputFile).splitLines; auto output File(outputFile, w); foreach (i, line; lines) { auto prompt line.strip; if (prompt.empty) continue; writeln(processing line , i 1); auto reply chatWithModel([{role:user,content: ~ prompt ~ }]); output.writeln(i 1, \t, prompt, \t, reply); } }建议批量任务采用输入一行 - 输出一行的方式中间任何失败都不影响后续内容。如果某一行请求超时记录日志后跳过最后统一重试。9. 资源占用与性能观察9.1 看什么指标运行 agent 时重点关注四个指标D 程序进程的 CPU 和内存占用。模型服务的 CPU 或 GPU 占用。模型服务的显存占用。单次请求的响应耗时。9.2 观察方法Linux 下可以开两个终端# 终端 1观察模型服务占用如果用的是 GPU watch -n 2 nvidia-smi # 终端 2观察 D agent 进程 ps aux | grep local-llm-agentWindows 下打开任务管理器在“进程”或“性能”标签页查看。9.3 影响因素模型参数量3B 和 7B 的显存占用差距很大。量化精度fp16、fp32、bf16、int8、int4 的占用逐级降低但输出质量也可能下降。上下文长度messages 越长显存占用越高。批量任务时尤其明显。并发数同时多个请求访问模型服务会显著增加显存占用和排队延迟。9.4 降低开销的手段用小参数模型。使用量化模型。限制最大上下文长度。工具返回内容截断。批量任务中控制单条长度。10. 常见问题与排查方法问题现象可能原因排查方式解决方案编译失败提示找不到依赖包dub 未联网下载依赖检查网络执行dub upgrade配置可用的 dub 镜像或手动放置依赖agent 连接 127.0.0.1:11434 报超时Ollama 服务未启动或端口被改检查端口监听状态启动ollama serve或修改 URL 端口模型回复非常慢CPU 推理或模型过大查看模型进程 CPU 占用换更小的量化模型或改到 GPU 推理多轮对话后回复跑偏上下文被裁剪或截断查看 history 长度和裁剪逻辑增大保留轮数或改用摘要压缩模型输出了 TOOL 但程序没执行工具名或参数格式不匹配打印 raw reply 检查格式严格校验 TOOL:name工具返回内容过长导致请求失败上下文超限查看工具返回值长度截断工具返回内容模型拒绝调用工具直接凭记忆回答系统提示词约束不够强查看返回内容强化提示词加入“必须调用工具”的指令某些模型 API 报 HTTP 400提示reasoning_content必须回传思考模式状态未完整传回查看模型服务 API 文档按 API 要求回传reasoning_content或关闭思考模式agent 中途异常退出上游响应格式变化查看 agent 日志增加异常捕获和重试逻辑本机配置了网络代理类规则请求 127.0.0.1 异常本地回环请求被代理规则拦截检查代理配置将 127.0.0.1 加入绕过列表或临时关闭代理11. 最佳实践与使用建议11.1 第一次先小参数测试第一轮运行不要直接上 7B 模型先用 0.5B 或 3B 量化模型跑通链路。链路通了之后再换大模型。这样可以快速区分“程序 bug”和“模型质量问题”。11.2 保留最小可运行配置把下面这几项固化下来方便回滚- dub.json 固定依赖版本 - 系统提示词版本 - 模型名称和量化方式 - 上下文裁剪参数11.3 目录管理建议采用以下结构bin/ 编译产物 config/ 提示词和配置 models/ 模型说明文件 data/ 输入素材 output/ 批量任务结果 logs/ agent 运行日志11.4 工具调用的安全边界file_read只允许读取工作目录下的文件。如果未来扩展shell_exec必须使用白名单不能把用户输入直接拼进 shell 命令。工具执行结果要控制长度防止模型被超长内容带偏。对模型返回的TOOL:行做严格格式校验解析失败时宁可放弃执行也不要执行错误指令。11.5 接口服务限制访问范围HTTP 服务默认绑定在127.0.0.1不要直接暴露到公网。如果必须对外提供服务前面要加鉴权层和访问白名单。11.6 批量任务要加日志与重试批量任务里任何一行请求失败都不能让整个任务退出。推荐记录处理行号、请求内容、返回内容、耗时、错误信息完成后统一分析失败原因。12. 总结与下一步这个项目最值得尝试的点不是“用 D 语言调模型”这个噱头而是那套自省机制让 agent 通过工具查询自身状态而不是靠模型瞎编。它把 agent 的能力边界变成了可查询、可验证的运行时数据这在工程上非常有价值。建议你最先验证三个功能单轮对话能通。llm_config自省链路能通。上下文裁剪后历史记录行为符合预期。最容易踩的坑有三个模型服务没启动导致连接超时、TOOL:格式解析不一致导致工具不执行、提示词约束不够强导致模型跳过工具直接回答。这三个问题都能通过看日志定位。跑通这个最小版本之后后续可以往这些方向扩展接入 llama.cpp 的 C API去掉 HTTP 中转。支持原生 function calling输出结构化工具调用。增加文件知识库让 agent 可以检索本地文档。把多个工具组合成任务链实现更完整的 agent 能力。把 HTTP 服务做成异步并发版本提高批量处理吞吐。建议收藏备用。先用小模型跑通链路再逐步替换模型、增加工具、接入业务你会对这个“AI 如何知道自己是谁”的过程有更深的体会。
返回列表