三种语言实现同一LLM Agent:工具调用主循环的跨语言建模
2026/9/4 17:48:40 网站建设 项目流程

最近和几个团队聊 LLM Agent 的架构选型,发现一个很典型的分歧:有人觉得 Python 加一个 Agent 框架就够了,有人坚持把 Agent 当作独立的领域模型来做,还有人已经在 Elixir 的进程模型里折腾“每个 Agent 一个进程”的方案。

我的建议是,在选定框架之前,先做一个更小、也更诚实的小练习:把同一个带工具调用的 LLM Agent,分别用 Python、Clojure、Elixir 写一遍,只做最核心的工具调用主循环。

这个练习看起来有点“炫技”,但实际做下来价值很高。你会发现,跨语言实现 Agent 得到的不是语法比较,而是同一时间看到 Agent 的三个切面:Python 所代表的命令式状态管理、Clojure 所代表的数据驱动递归、Elixir 所代表的消息并发模型。这层心智模型,换到任何 Agent 框架后都不会过时。

这篇文章把三份可运行的代码、环境准备、运行方法和工程建议完整拆开。无论你最后用不用 Clojure 或 Elixir,这套“工具调用、消息回填、循环终止”的核心逻辑都是绕不开的。

1. 为什么要用三种语言写同一个 LLM Agent

先明确一个容易混淆的问题:标题里的“Modeling”,不是让模型给自己建模,而是开发者如何用代码描述一个 LLM Agent 的运作方式。

很多刚接触 Agent 的开发者会把大量时间花在提示词上,觉得只要把 prompt 写得足够好,模型就能自动完成任务。但当你进入真实项目,会立刻发现瓶颈不在“模型会不会理解”,而在“你的代码能不能正确完成一次模型和工具之间的往返”。

例如用户问:“北京今天需要带伞吗?”

一次完整的 Agent 流程可能是:

  1. 模型先决定调用get_current_weather
  2. 模型返回一个结构化的工具调用,而不是直接输出“我不知道”。
  3. 你的代码执行天气查询,拿到结果。
  4. 代码把天气结果作为tool角色消息回填给模型。
  5. 模型阅读结果后,给出最终回答:“北京今天晴天,不需要带伞。”

这套流程无论用什么语言实现,本质都相同。但不同语言会让开发者以不同的方式思考状态、数据和并发:

  • Python 更适合快速实现“面向对象 + 可变状态”的直观流程;
  • Clojure 强制你把 Agent 当作不可变数据和递归函数来组织;
  • Elixir 则把 Agent 放到 BEAM 的进程模型里,让你天然关注隔离和并发。

所以,这篇文章真正解决的问题是:当你不依赖任何重量级 Agent 框架时,如何用最朴素的 HTTP 请求实现一个最小 Agent,并判断哪种语言的工程风格更适合你的团队。

如果你正准备从 Python 转向函数式语言,或者需要在现有 Clojure/Elixir 系统中接入 LLM,这篇文章可以直接给出一个最低可行实现。

2. LLM Agent 的基础共识:角色、消息、工具与主循环

在看代码之前,先把 Agent 的基础概念压缩到最小。

2.1 Agent 的核心组成

一个带工具调用能力的 Agent,至少包含四部分:

  • 模型:承担意图理解和工具选择,通常通过 Chat Completions 一类的接口调用。
  • 工具:模型可以调用的外部函数,例如天气查询、数据库查询、订单查询。
  • 消息历史:系统消息、用户消息、助手消息、工具消息按顺序组成的数组。
  • 主循环:判断模型返回的是最终文本还是工具调用,并循环执行。

把 Agent 简化成“模型 + 工具 + 循环 + 状态”,是跨语言建模最有效的思路。它避免了一上来就纠缠于复杂的多智能体框架。

2.2 工具调用协议:function calling

模型本身没有“执行函数”的能力。模型能做的是输出一个结构化 JSON,告诉你的代码“应该调用哪个函数、参数是什么”。

这套机制通常称为 function calling 或 tool calling。请求时,你把工具描述放在tools参数里:

[ { "type": "function", "function": { "name": "get_current_weather", "description": "查询某个城市的实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如:北京" } }, "required": ["city"] } } } ]

模型返回的内容里,可能包含tool_calls,例如:

{ "id": "call_123", "type": "function", "function": { "name": "get_current_weather", "arguments": "{\"city\": \"北京\"}" } }

注意,arguments是一个字符串,不是对象。落地时第一步是解析这个字符串,否则很容易出现 JSON 解析失败。

2.3 为什么消息顺序不能乱

工具调用要求消息顺序非常严谨。一次成功的历史应该长这样:

角色内容
system设定助手身份和约束
user北京今天需要带伞吗
assistant返回 tool_calls,而不是最终回答
tool对应上面的 tool_call_id,返回天气查询结果
assistant北京今天晴天,不需要带伞

容易出现的问题是两个:一是客户端收到tool_calls后,没有把 assistant 消息放回历史,二是执行完工具后,没有把tool消息放回历史。这两种错都会导致模型无法“看到”自己刚才调用过什么工具。

2.4 主循环的等价结构

如果用伪代码描述 Agent 主循环,几乎所有语言都是同一张图:

用户输入 → 请求模型 → 有 tool_calls? → 是:执行工具,把结果作为 tool 消息追加 → 否:返回最终文本 → 继续循环,直到达到最大轮数

后面的三份实现,本质上是同一张结构图在三种语言里的翻译。

3. 环境准备:一套 OpenAI 兼容协议跑三个运行时

为了减少环境差异,三份示例统一使用兼容POST /v1/chat/completions的接口。目前很多模型服务商和本地推理工具都支持这个协议,你只需要把地址、模型名、密钥替换成自己的。

示例中使用以下环境变量:

export LLM_BASE_URL="http://localhost:11434/v1" export LLM_MODEL="your-model-name" export LLM_API_KEY="your-api-key"

如果你的服务不需要密钥,也可以填任意值,但建议保留变量,避免代码里硬编码密钥。

三个语言的基础环境要求如下:

语言运行环境主要依赖
PythonPython 3.9 以上requests
ClojureJDK 11 以上、Clojure CLIclj-http、cheshire
ElixirErlang/OTP 25 以上Req、Jason

如果你只是想把流程先跑通,我建议先用本地模型或公司内部已部署的模型服务,不要在公共网络环境中随意泄露 API Key。密钥只能通过环境变量提供,不要提交到 Git 仓库。

本文重点关注通用思路,所以模型名保持为占位符,实际使用时请替换成你的模型服务商可用的值。

4. 用 Python 建模:最小依赖的命令式 Agent

Python 是 Agent 生态最丰富的语言,但这并不意味着必须依赖 LangChain 之类的框架。先用自己的代码实现一遍主循环,反而能避免“被框架遮住核心逻辑”。

下面是一个只依赖requests的最小实现。

# 文件:python_agent.py import json import os import requests MODEL = os.getenv("LLM_MODEL", "your-model-name") API_KEY = os.getenv("LLM_API_KEY", "") BASE_URL = os.getenv("LLM_BASE_URL", "http://localhost:11434/v1") MAX_STEPS = 5 TOOLS = [ { "type": "function", "function": { "name": "get_current_weather", "description": "查询某个城市的实时天气", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名,例如:北京" } }, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "get_product_price", "description": "查询某个商品的当前价格", "parameters": { "type": "object", "properties": { "product_id": { "type": "string", "description": "商品编号" } }, "required": ["product_id"], }, }, }, ] def get_current_weather(city: str) -> str: # 实际项目中,这里应该调用真实的天气服务 return json.dumps( {"city": city, "weather": "晴", "temperature": 22}, ensure_ascii=False, ) def get_product_price(product_id: str) -> str: # 实际项目中,这里应该查询商品系统 return json.dumps( {"product_id": product_id, "price": 499, "currency": "CNY"}, ensure_ascii=False, ) def execute_tool(tool_name: str, arguments_text: str) -> str: """把模型输出的 arguments 字符串解析成真正的函数参数。""" try: args = json.loads(arguments_text) if tool_name == "get_current_weather": return get_current_weather(**args) if tool_name == "get_product_price": return get_product_price(**args) return json.dumps({"error": f"unknown tool

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询