最近在 Hacker News 上看到一个很有意思的项目:Linkly。它的定位非常直接——“a language designed for LLMs, compiled through MLIR”,也就是一门专门为 LLM(大语言模型)设计的编程语言,并且通过 MLIR 完成编译。第一次看到这个描述,很多开发者的第一反应是:为什么还需要一门新语言?LLM 写 Python、TypeScript、SQL 不香吗?但等你真正接触 LLM 应用开发、Agent 编排、结构化输出、工具调用这些场景后,就会慢慢理解这类语言存在的价值。
本文将围绕 Linkly 的设计动机、MLIR 在其中的作用、编译链路如何理解,以及后续可以怎样动手实践来展开。无论你是刚接触 LLM 应用开发,还是已经在做 Agent、RAG、MCP 相关项目,都可以从中获得一些启发。
1. 背景与核心概念
1.1 当前 LLM 编程的痛点
先看一个最常见的场景:你想让 LLM 完成一个任务,例如“读取某个接口的数据,经过清洗后写入数据库”。直接用自然语言描述,模型可能返回一段 Python 代码,也可能返回一段 JSON,还可能直接输出一段 Markdown。你需要额外编写 prompt 约束输出格式,再写解析逻辑,还要处理大量异常情况。
这些问题可以概括为以下几点:
- 自然语言输出不稳定,无法保证语法正确。
- 模型生成的代码灵活性太高,容易超出预期边界。
- 工具调用、参数校验、权限控制需要额外封装。
- 长任务场景下,模型容易“发散”,需要强结构约束。
- 现有语言对 LLM 并不友好,语法复杂,token 开销大。
这些痛点推动了一个方向:设计一种“让 LLM 更容易生成、让编译器更容易检查”的语言。Linkly 正是这个方向的实践。
1.2 Linkly 是什么
从项目名称和描述来看,Linkly 不是一个大而全的通用编程语言,而是面向 LLM 场景的语言。它的核心思路可以理解为:
- 提供一种表达力足够、但约束更强的语法。
- 让 LLM 的输出天然符合语言规范。
- 通过编译器对生成结果进行静态检查、优化和翻译。
- 将最终逻辑映射到底层可执行环境,而这个过程依赖 MLIR。
这里的“LLM”并不是指语言模型本身作为运行环境,而是指这门语言的设计目标:为 LLM 生成代码、与 LLM 交互、编排 LLM 行为而服务。
1.3 MLIR 是什么
MLIR(Multi-Level Intermediate Representation,多级中间表示)是 LLVM 生态中的一个编译器基础设施项目。它不是一个单一 IR,而是一套可扩展的 IR 框架,允许你在不同抽象层级之间做翻译、优化和降级。
传统编译器链路通常是:
源代码 -> AST -> 中间表示(IR) -> 优化 -> 机器码MLIR 把这个过程拆得更细,它允许你自定义 dialect(方言),也就是自定义 IR 的语义和操作。你可以用高层方言描述业务逻辑,用低层方言描述硬件指令,然后通过 pass 把高层方言逐步降低到低层方言。
Linkly 选择 MLIR,主要看重以下几点:
- 多层抽象能力,可以把语言高层结构逐步降级为可执行代码。
- 丰富的 pass 基础设施,便于做合法性检查、优化和转换。
- 与 LLVM 生态打通,可以复用大量成熟后端。
- 便于对接不同硬件后端,包括 CPU、GPU 甚至专用加速器。
2. 为什么“为 LLM 设计语言”值得关注
2.1 传统语言对 LLM 不够“友好”
我们常用的 Python 或 JavaScript,语法灵活,但也正因为灵活,LLM 生成时很容易产生偏差。例如:
- 引号不匹配。
- 缩进混乱。
- 类型隐式转换带来的歧义。
- 库函数调用方式错误。
- 逻辑不完整,需要人工补齐。
你可以通过 prompt 约束,但 prompt 本质上不是“语法约束”,模型仍会犯错误。更好的做法是把约束内建到语言本身,让模型在生成时天然遵循一个受限的语法子集。
2.2 语言即约束
当你设计一门面向 LLM 的语言时,你可以故意去掉一些不必要的特性:
- 不提供动态类型。
- 不提供复杂的继承。
- 不提供无限递归。
- 对工具调用做一等公民支持。
- 对结构化输出做语法级支持。
这样 LLM 的生成空间被收窄,正确率会明显提升。编译器也能在生成后立刻发现结构性问题,而不是等到运行时才暴露。
这有点像“专门设计一门 DSL(领域特定语言)”,只不过使用者和生成者都是 LLM。
2.3 编译器带来的可控性
LLM 直接生成自然语言或通用代码,我们很难做到可控。但如果经过编译器,流程就变成:
LLM 输出 Linkly 源码 -> 编译器解析 -> 编译通过 -> 运行编译通过意味着语法正确、类型正确、调用关系正确。这样 LLM 的错误就不会传导到运行时。更重要的是,编译器可以在编译期注入安全检查、资源限制和行为审计。
这也是 Linkly 选择 MLIR 的一个原因:MLIR 的 pass 体系适合做各种静态分析和安全检查。
3. 环境准备与项目结构
3.1 环境说明
由于 Linkly 当前可能是早期项目,具体版本和安装方式需要以官方仓库为准。这里我给出一个通用的实验环境思路,适合在自己本机实践 LLM DSL + MLIR 相关开发。
操作系统方面,建议使用 Linux 或 macOS。Windows 也可以,但需要额外配置 WSL 或原生工具链。本文示例以 Linux 为主。
建议准备以下工具:
- CMake:3.20 以上版本。
- Ninja:用于加速构建。
- Clang / GCC:作为宿主编译器。
- LLVM/MLIR 开发库:可以从源码构建,也可以使用包管理器安装。
- Python 3.9+:用于编写实验脚本,调用 LLM API。
- Git:用于克隆项目。
3.2 安装基础依赖
以 Ubuntu/Debian 为例,可以执行:
sudo apt update sudo apt install -y cmake ninja-build clang lld llvm-dev git python3 python3-pip如果你在 macOS 上,可以使用 Homebrew:
brew install cmake ninja llvm git python需要说明的是,不同发行版的 LLVM/MLIR 版本差异较大。建议先确认你安装的 LLVM 版本。
llvm-config --version如果项目要求特定版本,源码构建会更可靠。MLIR 源码构建方式如下:
git clone https://github.com/llvm/llvm-project.git cd llvm-project mkdir build cd build cmake -G Ninja ../llvm \ -DLLVM_ENABLE_PROJECTS="mlir" \ -DLLVM_TARGETS_TO_BUILD="host" \ -DCMAKE_BUILD_TYPE=Release ninja这部分构建时间较长,依赖机器性能。如果你只是了解概念,建议直接使用系统包,减少编译时间。
3.3 示例项目结构
当我们基于 Linkly 做实验时,项目结构可以规划为:
linkly-lab/ ├── CMakeLists.txt ├── README.md ├── examples/ │ ├── hello.linkly │ └── tool_call.linkly ├── src/ │ ├── main.cpp │ ├── LinklyDialect.cpp │ └── LinklyDialect.h ├── include/ │ └── Linkly/ │ └── LinklyDialect.h └── scripts/ ├── generate_dataset.py └── eval_output.py这不是 Linkly 官方的项目结构,而是针对“尝试实现一个类 Linkly 语言”的常见工程组织方式。如果你只是使用 Linkly 的编译器,那只需要关注 examples 目录和可执行文件位置。
4. Linkly 语言设计思路拆解
4.1 核心语言特性
结合 LLM 场景,可以推测 Linkly 会具备以下特征:
- 严格类型申明,不依赖类型推导,减少模型猜测。
- 结构化控制流,例如固定格式的条件语句和循环。
- 工具调用语法,让 LLM 可以明确表达“我要调用某个函数”。
- 结构化输出语法,例如内置 JSON 对象字面量。
- 禁止复杂表达式,避免模型生成难以解析的逻辑。
为了便于理解,我构造一个示意代码。注意,这不是 Linkly 官方语法,只是为了说明设计思路:
// 文件:examples/tool_call.linkly // 示意代码,不代表真实 Linkly 语法 task fetch_and_save { input url: string input output_file: string let data = call http.get(url) -> { status: int, body: string } if data.status == 200 { write_file(output_file, data.body) } else { log("request failed: " + data.status) } }这段代码表达了一个简单任务:请求 URL,根据状态码决定写文件或打印日志。它有几个特点:
- 变量有明确类型。
call http.get(url)是工具调用,返回结构化对象。- 控制流结构清晰,没有隐式类型转换。
- 每一行都能被编译器严格检查。
如果 LLM 被要求生成这种结构的代码,它的自由度就比生成 Python 小得多,正确率自然更高。
4.2 MLIR 方言映射
Linkly 源码最终会通过 MLIR 编译,那它如何映射到 MLIR 呢?通常流程是:
Linkly 源码 -> AST -> Linkly Dialect -> Standard Dialect -> LLVM Dialect -> 机器码其中 Linkly Dialect 是 Linkly 自己定义的 MLIR 方言。比如上面的工具调用,可能被表示为一个linkly.call_tool操作:
%result = linkly.call_tool @http_get { url = "https://example.com" } : (!linkly.string) -> !linkly.struct<status: i32, body: string>当然,这只是示意。实际方言定义会复杂得多。
MLIR 的好处是,你可以先在高层的 Linkly Dialect 上做业务级检查,比如:
- 工具调用参数是否匹配。
- 类型是否合法。
- 输出结构是否被正确使用。
- 是否存在未授权的敏感操作。
然后通过 pass 将高层操作逐步降低到通用 IR。
4.3 编译器既做检查,也做翻译
传统编译器关注性能优化,而 Linkly 这类编译器更关注“语义安全性”和“LLM 对齐”。
举个例子,编译器可以在编译期拦截以下问题:
- 模型要求调用一个不存在的工具。
- 参数类型错误。
- 输出 schema 不符合要求。
- 访问了被禁止的系统路径。
- 缺少必要的鉴权字段。
这些检查如果放在运行时,会很晚才暴露。放在编译期,就能在代码执行前发现问题。
5. 从零构建一个“类 Linkly”实验
5.1 先定义语言子集
我们不需要完整实现 Linkly,可以先实现一个极简子集,帮助理解 MLIR 的作用。假设我们的 mini 语言只支持:
- 整数类型。
- 加法操作。
- 变量绑定。
- 输出操作。
语言源码示例:
let x = 1 + 2 print(x)我们可以用 Python 写一个非常简单的解析器,把这段代码转成 MLIR 文本表示。这里只是为了展示思路,不是生产代码:
# 文件:scripts/mini_compiler.py # 功能:将 mini 语言源码转成类 MLIR 文本表示 # 注意:这是简化示例,不是真实 MLIR 生成器 def parse_mini(source: str): lines = [] for line in source.strip().splitlines(): parts = line.strip().split() if parts[0] == "let": # let x = 1 + 2 var_name = parts[1] expr = " ".join(parts[3:]) lines.append((var_name, expr)) elif parts[0] == "print": lines.append(("print", parts[1])) else: raise SyntaxError(f"unknown syntax: {line}") return lines def to_mlir(ast): mlir_lines = [] for item in ast: if item[0] == "print": mlir_lines.append(f" llvm.call @print({item[1]})") else: var_name = item[0] # 这里简单地把算术表达式原样输出 mlir_lines.append(f" %{var_name} = arith.addi {item[1]}, {item[1]} : i64") return "\n".join(mlir_lines) source = """ let x = 1 + 2 print(x) """ ast = parse_mini(source) print(to_mlir(ast))运行上面的代码,会得到类似输出:
%x = arith.addi 1 + 2, 1 + 2 : i64 llvm.call @print(x)这个输出并不是合法 MLIR,因为arith.addi的操作数写法不对。这里只是演示“源码到中间表示”的映射思路。真实实现需要为变量分配 SSA 值,并且要把1 + 2解析为两个常量相加。
正确的简化 MLIR 思路应该是:
%0 = arith.constant 1 : i64 %1 = arith.constant 2 : i64 %2 = arith.addi %0, %1 : i64 print %2从这个小实验可以看出,即使是一门极简语言,要让 MLIR 正确处理,也需要做 AST 拆分、常量声明、SSA 值管理等操作。这侧面说明 Linkly 用 MLIR 编译并不是一件简单的事,但 MLIR 确实提供了成熟的底层基础设施。
5.2 设计 LLM 可用的输出约束
对 Linkly 来说,很重要的一个环节是让 LLM 输出符合语法。我们可以借鉴这个思路,设计一个“受限 JSON 协议”,让模型以填空方式生成结构化代码。
例如,给 LLM 的 prompt 要求它输出以下 JSON 结构:
{ "task": "fetch_and_save", "params": { "url": "https://example.com", "output_file": "output.txt" }, "tool_calls": [ { "tool": "http.get", "args": { "url": "https://example.com" } }, { "tool": "write_file", "args": { "path": "output.txt", "content_from": "http.get.body" } } ] }这种格式虽然不是语言源码,但它是“面向 LLM 的中间表达”。Linkly 的编译器可以接受这种结构化表达,然后把它转换为 MLIR 高层方言。这也是 Linkly 与普通编程语言的重要区别:输入来源可能是模型,而非人。
5.3 接入大模型的实验流程
如果我们想验证“Linkly 语法是否比自然语言更容易被模型生成”,可以设计一个实验:
- 准备一批任务描述,例如“读取一个文件并统计行数”。
- 准备两种输出格式:纯自然语言说明、Linkly 风格代码。
- 使用同一 LLM 分别生成。
- 比对解析成功率、语法正确率、运行正确率。
示例评估脚本框架如下:
# 文件:scripts/eval_output.py # 作用:统计 LLM 输出是否符合目标 schema import json def validate_linkly_output(text: str) -> bool: try: data = json.loads(text) if "task" not in data: return False if "tool_calls" not in data: return False return isinstance(data["tool_calls"], list) except json.JSONDecodeError: return False # 模拟一次评测 fake_output = '{"task": "fetch", "tool_calls": []}' print(validate_linkly_output(fake_output))这个脚本只能说明最基本的 JSON 有效性检查,真实场景还需要校验参数类型、依赖关系、工具权限等。但流程是一样的:先检查语法,再检查语义,最后再决定是否执行。
6. 面向 LLM 的语言编译落地思考
6.1 与 Agent 编排的关系
现在很多人用 LangChain、Spring AI 或自研 Agent 框架来做任务编排。它们通常会定义一套“工具调用协议”,但大多是在自然语言与 JSON 之间做转换。
Linkly 的路线不同:它把任务描述本身变成一门语言,然后通过编译器来验证和优化。这相当于把 Agent 的“思考过程”和“执行计划”放进一个有约束的语法空间里。
这并不意味着 Linkly 会替代 Agent 框架。更可能的是,Linkly 可以作为一个“内部任务表达层”,承接 LLM 输出,再由编译器生成可执行的调用计划。
6.2 与安全边界的关系
LLM 直接生成代码并执行,安全风险很大。轻则产生 bug,重则导致数据泄漏或系统破坏。Linkly 这类语言如果能把敏感操作限制在编译器层面,会是一个很好的方案。
具体来说,安全边界可以体现在:
- 白名单工具集合。
- 资源配额限制。
- 禁止访问内部网络地址。
- 输出内容长度限制。
- 每次工具调用前自动注入审计日志。
- 对文件系统做路径拦截。
这些检查可以在编译期或运行期执行。MLIR 的 pass 机制很适合做编译期检查,运行期则可以通过 runtime 模块做二次确认。
6.3 与 LLM 精度问题的关系
网上经常讨论 LLM 大模型的精度问题,比如 FP16、FP32、BF16 的选择。这看起来和 Linkly 没有直接关系,但如果 Linkly 最终要被编译到 GPU 或其他加速器上,精度问题就会涉及。
MLIR 提供了丰富的数值类型和转换能力,可以更早地描述“这个计算使用浮点还是半精度”,并在编译阶段进行精度相关优化。如果 Linkly 未来支持数值计算任务,MLIR 在精度管理上的优势就会显现。
6.4 与 MCP 连接的关系
MCP(Model Context Protocol)是最近很热的话题,它主要解决 LLM 应用与外部工具、数据源之间的标准化连接问题。Linkly 如果支持工具调用,就可以把 MCP 工具封装为 Linkly 的可调用函数。
一种可能的关系是:
LLM -> Linkly 源码 -> Linkly 编译器 -> 工具调用协议 -> MCP Server -> 外部工具这样 Linkly 成了“LLM 与 MCP 之间的一层可编译接口”,让工具调用不再是随意的 JSON,而是经过编译器验证的强类型调用。
7. 常见问题与排查思路
在实际尝试类 Linkly 项目或 MLIR 编译流程时,可能会遇到一些典型问题。下面以表格形式列出排查思路。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
| 构建失败,提示找不到 MLIR 头文件 | LLVM/MLIR 未安装或路径未配置 | 检查llvm-config --includedir,确认 include 目录已加入 CMake 搜索路径 |
| 版本不兼容,编译报错函数签名不一致 | 本机 LLVM 版本与项目要求不一致 | 使用项目要求的 LLVM 版本,必要时源码构建 |
| LLM 输出不符合 Linkly 语法 | prompt 约束不足,缺少 few-shot 示例 | 提供语言语法示例,并限定输出格式 |
| 编译通过但运行时不执行 | 高层方言未完全降级到底层方言 | 检查 pass pipeline,打印mlir-opt --mlir-print-ir-after-all |
| 工具调用被拒绝 | 安全策略限制了目标工具 | 检查白名单策略与权限配置 |
| 变量类型不匹配 | 类型推断与声明不一致 | 在代码生成阶段显式为每个变量附加类型信息 |
| 长任务生成后 token 超限 | 语言表达偏冗余 | 缩减 prompt 中的示例数量,或压缩语言关键字长度 |
| 内存占用过高 | MLIR 构建 debug 模式导致 | 使用-DCMAKE_BUILD_TYPE=Release重新构建 |
这里强调一点:如果你在使用某个具体开源项目,请先查看官方 README 和 issue 区,不要盲目修改配置。MLIR 的报错信息通常比较难读,先定位是 parse 错误、type 错误还是 pass 错误,再针对性解决。
8. 最佳实践与工程建议
8.1 从极小子集开始
不要一开始就设计一套复杂的语言。建议先定义 10 到 20 个关键字,支持基本的顺序执行、条件判断、循环调用和工具调用即可。等实验稳定后,再逐步增加特性。
这种“最小可用 DSL”的方式,能让 LLM 更容易掌握,也能让编译器实现更简单。
8.2 为 LLM 提供高质量示例
语言设计得再好,如果模型的 few-shot 示例不够清晰,依然会产生偏差。建议在 prompt 中提供三到五个典型示例,覆盖:
- 简单任务。
- 多工具调用。
- 条件分支。
- 异常处理。
- 结构化输出。
示例代码要短小精悍,最好和真实任务高度相关。
8.3 编译错误信息要可读
LLM 生成代码后,编译器返回的错误信息如果太底层,对调试没有帮助。建议在上层捕获 MLIR 错误,并转化为人类可读的形式。
例如:
错误:第 3 行,变量 status 的类型是 int,但 write_file 需要 string 类型。而不是直接抛出:
error: 'linkly.call_tool' op operand type mismatch可读错误信息对开发者和模型迭代都非常重要。
8.4 强化执行沙箱
即使编译器做了检查,运行时依然要保留安全边界。建议:
- 通过 Docker 或容器执行生成的代码。
- 限制网络访问,只允许特定域名。
- 限制文件系统访问,只开放临时目录。
- 对工具调用进行审计日志记录。
- 设置超时时间和资源限制。
这样即使编译期出现了遗漏,运行时也不会造成大规模破坏。
8.5 建立评估集
面向 LLM 的语言,需要持续评估“模型是否能稳定生成合法代码”。建议建立一份离线评估集,包含不同难度的任务。每次修改语言或编译器后,都跑一遍评估集,统计以下指标:
- 语法解析成功率。
- 编译通过率。
- 运行成功率。
- 输出与预期的一致性。
- 平均 token 消耗。
这些指标可以帮你判断语言改动是变好了还是变差了。
8.6 关注版本变化
LLVM 和 MLIR 的 API 迭代速度非常快,很多接口在一个版本可用,下一个版本就废弃了。如果你是在项目中使用 Linkly,建议锁定 LLVM 版本,并把依赖版本写入构建脚本。同时关注上游项目的更新日志,避免被新版本破坏构建。
9. 总结与后续学习路线
Linkly 这类“面向 LLM 设计、通过 MLIR 编译”的语言,本质上是在解决一个大问题:如何让 LLM 的输出从“不可控的自然语言”变成“可编译、可验证、可优化、可安全执行的程序”。
通过前面的分析,你已经了解了:
- LLM 生成现有语言代码的痛点。
- Linkly 语言设计思路:语法约束、工具调用、结构化输出。
- MLIR 在编译链路中的位置:高层方言、下降、优化、后端生成。
- 如何搭建 DIY 实验环境。
- 如何设计一个极简编译器映射思路。
- 常见错误排查和工程化建议。
如果你想进一步深入,可以从以下方向继续学习:
- 阅读 MLIR 官方 Toy 教程,理解 dialect 和 pass 的基础概念。
- 编写一个小的 DSL,并用 Python 转成合法 MLIR。
- 尝试用 LLM 生成受限 JSON DSL,再编写解析器。
- 研究 MCP 协议,看它如何与工具调用语言结合。
- 关注 Linkly 项目后续进展,拿到官方文档和示例后再做真实集成。
很多新的技术方向,初期看起来像“重新发明轮子”,但实际上是在复杂系统里探索新的抽象边界。Linkly 的尝试可能不会成为通用标准,但它背后“用编译器的思路约束 LLM 输出”这个方向,值得每个 LLM 应用开发者认真思考。
如果你正准备做 LLM Agent 或工具编排系统,不妨尝试在自己的项目里设计一个最小约束层,先不求完整编译器,只求让模型输出更稳、更可解析。这是迈向“LLM 工程化”很有效的一步。