微信扫码
添加专属顾问
我要投稿
探索AI模型如何执行复杂代码,实现智能化任务。 核心内容: 1. AI模型的高级能力:理解意图并执行复杂指令 2. 实现Function Calling机制,让模型调用工具和运行代码 3. 后端系统如何解析并执行模型请求的Python代码
在开发 MolaGPT 的过程中,我始终认为,单纯的文本对话只是 AI 模型能力的一种表象。而真正激动人心的是模型能够主动理解意图,和人类一样制定计划,执行复杂指令来解决复杂问题,比如调用工具、生成图表、分析数据等。
要实现这一点,就需要为模型构建一套完整的“工具箱”,还需要有强大的语言模型本身具有调用工具的能力,而这些能力就需要配套的后端接口和稳定的结果输出。
这段时间,我终于有机会着手开发一个基于 Function Calling(现在貌似 Tools Calling)的 Agent. 我的目标很明确:首先,赋予模型联网搜索的能力;其次,也是更关键的,让模型能够运行 Python 代码。
手搓复刻 OpenAI 的 Function Calling
我的后端整体的核心思路借鉴了 OpenAI 的 Function Calling 机制。其原理可以概括为:当模型识别到用户意图需要借助外部工具才能完成时,它不再直接生成最终答案,而是生成一个结构化的 JSON 对象。这个 JSON 对象精确描述了需要调用的函数名称(例如 execute_python_code)以及执行该函数所需的参数(例如,一段 Python 代码字符串)。
这个结构化的请求随后被发送到我的后端系统。后端接收到这个请求后,会解析 JSON 内容,根据 name 字段匹配预先定义好的可用函数列表 ($available_functions),找到对应的处理逻辑(比如说 execute_python_code)。后端执行完任务后,再将执行结果(比如代码的标准输出或错误信息)返回给模型进行二次请求,随后模型结合上下文,生成最终回复给用户。
举个例子,如果模型判断需要执行 Python 代码 print(2+2),它会向后端发送类似以下的 JSON:
{ "name": "execute_python_code", "arguments": { "code": "print(2+2)" }}
后端解析出 name 为 execute_python_code,提取 arguments 中的 code 字段 "print(2+2)",然后将其交给一个隔离的 Docker 沙箱环境中的 Python 解释器执行。执行完毕后,捕获其 stdout,并将 "4" 这个结果返回给模型。这通常涉及两次与模型的交互:一次是模型输出 Function Call 请求,一次是后端返回执行结果供模型参考。
例如,在我的后端实现中,我为模型定义了两个核心函数:
search_web:用于联网搜索最新信息。
execute_python_code:用于执行 Python 代码,也是本文讨论的重点。
这是后端定义的函数描述信息,用于告知模型这两个工具的存在和使用方法:
$available_functions = [ [ "type" => "function", "function" => [ "name" => "search_web", "description" => "在网络上搜索最新信息,获取权威内容 (Search the web for the latest information and authoritative content)", "parameters" => [ "type" => "object", "properties" => [ "query" => [ "type" => "string", "description" => "搜索查询内容 (The search query)" ] ], "required" => ["query"] ] ] ], [ "type" => "function", "function" => [ "name" => "execute_python_code", "description" => "运行一段 Python 代码,并返回标准输出结果 (Execute a snippet of Python code and return the standard output)", "parameters" => [ "type" => "object", "properties" => [ "code" => [ "type" => "string", "description" => "要执行的 Python 代码 (The Python code to execute, e.g., print(2+2))" ] ], "required" => ["code"] ] ] ]];
模型会根据对话上下文,智能判断是否需要调用这些工具,并自动生成相应的参数请求,交由后端执行。
过程中遇到的几个问题和解决
1. 模型并非总会使用 print()
在初步测试 execute_python_code 函数时,我发现模型生成的代码经常执行失败。查看 Log 发现,问题在于模型生成的代码很多时候没有显式地使用 print() 函数来输出最终结果,模型也许把后端的 Python 环境当做了Jupyter Notebook 这种交互式环境了。例如,当我让模型计算 2+2 时,它可能会生成如下代码并请求执行:
2 + 2
## 或者
result = 2+2
result
在 Jupyter Notebook 等交互环境中,这么写确实没问题,可是我的后端是单纯的拉取 Python 镜像人为构建的 Docker 环境,Python 执行器运行这段代码之后,由于没有 print()
语句,标准输出就为空。这样一来,模型就无法接收到计算结果 "4",导致其认为代码执行失败。
我最初的想法很简单:是不是可以在接收到的代码字符串末尾自动加上 print(...)
?但很快意识到这种方法过于粗暴。并非所有代码执行都需要打印最后一个表达式的结果,例如代码可能只是定义函数、导入库等等,确实不需要 print()
的存在。强行添加 print()
可能导致语法错误或非预期的行为。
经过一番资料查阅和向大模型“请教”,我决定采用 AST(Abstract Syntax Tree,抽象语法树)来智能地处理这个问题。AST 可以将代码解析成一个树状结构,其中的每个节点代表程序中的一种语法元素,比如表达式、语句、函数定义等。这个树的根是整个模块,而它的子节点可能是赋值语句、函数调用、条件判断等结构。
使用 AST 的主要目的就是精确地表达代码的语法和语义,忽略掉注释、空行等非实质性内容。而不是像之前那样去暴力添加 print()
.
import ast
class AutoPrintTransformer(ast.NodeTransformer):
"""
自动为最后一个表达式添加 print() 的 AST 转换器
"""
def __init__(self):
super().__init__()
self.modified = False
def visit_Module(self, node):
"""
处理模块节点,为最后一个表达式添加 print()
"""
self.generic_visit(node)
last_expr_index = -1
for i in range(len(node.body) - 1, -1, -1):
stmt = node.body[i]
# 判断是否为 print 语句
is_print_stmt = (
isinstance(stmt, ast.Expr) and
isinstance(stmt.value, ast.Call) and
isinstance(stmt.value.func, ast.Name) and stmt.value.func.id == 'print'
)
is_docstring = (
isinstance(stmt, ast.Expr) and
isinstance(stmt.value, ast.Constant) and
isinstance(stmt.value.value, str) and i == 0
)
if not is_print_stmt and not is_docstring:
last_expr_index = i
break
# 找到了需要添加 print 的表达式
if last_expr_index != -1:
expr_node = node.body[last_expr_index]
print_call = ast.Call(func=ast.Name(id='print', ctx=ast.Load()), args=[expr_node.value], keywords=[])
print_stmt = ast.Expr(value=print_call)
ast.copy_location(print_stmt, expr_node)
node.body[last_expr_index] = print_stmt
self.modified = True
return node
这段代码的主要执行流程为:
ast
库,通过 ast.parse()
将模型生成的代码字符串解析成一个 AST 对象。(node.body)
。ast.Expr
的语句节点。print()
print()
调用,或者是否是一个字符串常量。print()
函数调用节点中,并替换掉原来的表达式语句节点。ast.unparse()
将修改后的 AST 转换回 Python 代码字符串,或者直接编译并执行。通过这种方式,即使模型没有写 print()
,我们的后端也能找到所有漏掉的位置并动态补齐 print()
,确保计算结果能够被正确捕获并返回。
1.3 随后遇到的新问题
在上线一段时间后,我发现 PHP 在处理 Python 代码的引号和换行时存在严重问题。由于直接将 Python 代码塞进 Python 模板字符串时,经常因为代码内部含有特殊字符(如单引号、双引号、反斜杠等)或复杂的多行结构,导致字符串提前中断或发生语法错误。这是我始料未及的,因为之前做 PHP 开发时也没有直接在 PHP 代码中写 Python,没想到会有这种问题。
为彻底解决这一问题,我决定进一步优化:在 PHP 中先将用户的 Python 代码进行 Base64 编码,这样就能确保传递到 Python 环境中的字符串是原生的且不受特殊字符干扰。
1.4 现行解决方案:利用 Base64 来保持代码完整性,随后再利用 AST 自动补全 print()
后端在接收模型提交的 Python 脚本后,使用 PHP 的内置函数 base64_encode
对 Python 代码进行编码。
$b64 = base64_encode($user_python_code);
Python 侧模板不再直接使用三重引号夹入用户脚本,而是接收一个 Base64 字符串,然后通过 Python 的内置库 base64.b64decode()
将其还原成原始代码,再利用之前开发的 AST 自动补全 print()
的方法进行处理。
import base64, ast, traceback
class AutoPrintTransformer(ast.NodeTransformer):
def __init__(self):
self.has_modified = False
def visit_Module(self, node):
self.generic_visit(node)
last_expr = None
for i in range(len(node.body) - 1, -1, -1):
stmt = node.body[i]
if isinstance(stmt, ast.Expr) and not (
isinstance(stmt.value, ast.Constant) and isinstance(stmt.value.value, str)
):
last_expr = (i, stmt)
break
if last_expr:
idx, expr = last_expr
is_print = (
isinstance(expr.value, ast.Call) and
isinstance(expr.value.func, ast.Name) and
expr.value.func.id == 'print'
)
if not is_print:
print_node = ast.Expr(
value=ast.Call(
func=ast.Name(id='print', ctx=ast.Load()),
args=[expr.value],
keywords=[]
)
)
ast.copy_location(print_node, expr)
node.body[idx] = print_node
self.has_modified = True
return node
def transform_code(src):
try:
tree = ast.parse(src)
transformer = AutoPrintTransformer()
new_tree = transformer.visit(tree)
ast.fix_missing_locations(new_tree)
if transformer.has_modified:
exec(compile(new_tree, '<string>', 'exec'), globals())
else:
exec(src, globals())
except Exception as e:
print(f"处理代码时出错: {e}")
traceback.print_exc()
exec(src, globals())
# 从 Base64 解码恢复原始用户代码
user_code = base64.b64decode('<Base64_encoded_string>').decode('utf-8')
transform_code(user_code)
上述解决方法上线后,避免了 PHP 与 Python 代码交互过程中字符串意外截断的问题,带有复杂换行结构的 Python 可正确执行。
为了增强代码执行能力,我在构建 Docker 镜像时特意预制了 matplotlib 库,期望模型能够生成数据可视化图表。在测试中,模型确实能生成标准的绘图代码(例如绘制正弦波),并且也调用了 plt.show(),但是代码执行后,前端聊天界面却看不到任何图像,不过想想也对,这是当然的,因为 plt.show() 会尝试在图形用户界面(GUI)环境中打开一个窗口来显示图像。但在 Docker 沙箱中没有显示器或窗口系统,纯纯就是无头的环境 plt.show() 会静默失败或报错。
有人会觉得(这个人就是我):那能不能在 Prompt 中让模型保存图片呢?可这就出现了一个新的问题:即使模型改为调用 plt.savefig('plot.png') 将图像保存到文件,这个文件也只是存在于容器内,无法被外界直接访问,更何况一旦代码运行完毕,容器销毁之后数据就会丢失。
前置代码注入:执行模型代码前,在 header 区域注入一段作为补丁的代码,追踪所有创建的 Figure 对象,并设置 matplotlib 使用非交互式后端:
import os
import uuid
import matplotlib
matplotlib.use('Agg') # 使用非交互式后端
import matplotlib.pyplot as plt
fig_list = [] # 用于跟踪所有创建的图
orig_figure = plt.figure # 保存原始的 plt.figure 方法
def track_figure(*args, **kwargs):
"""
寻找生成的图形。
"""
fig = orig_figure(*args, **kwargs)
fig_list.append(fig)
return fig
plt.figure = track_figure
后置代码注入:在代码执行结束后,遍历所有图像对象,将它们保存为 PNG 文件,透传到本地目录中,并将路径转换为公网可访问的图像链接:
...
saved_paths = []
if 'fig_list' in globals() and fig_list:
output_dir = "/output" # 输出目录
os.makedirs(output_dir, exist_ok=True)
for i, fig in enumerate(fig_list):
unique_id = str(uuid.uuid4())[:8] # 生成唯一 ID
filename = os.path.join(output_dir, f"chart_{i}_{unique_id}.png") # 构建文件名
try:
fig.tight_layout()
except Exception:
pass
try:
fig.savefig(filename, dpi=100, bbox_inches='tight') # 保存图像
saved_paths.append(filename) # 添加路径到列表
except Exception as e:
print("出错!!")
finally:
plt.close(fig) # 关闭图形
if saved_paths:
for img_path in saved_paths:
print(f"###IMAGE_FILE_PATH###{img_path}")
else:
print("没有图表。")
plt.close('all'
模型处理逻辑:并使用着重符号 ###IMAGE_FILE_PATH### 来提示模型当前位置有图片,使用让模型可以理解的自然语言来输出 Markdown 格式的图表:

一系列处理后,模型所绘制的图像就能在前端被展示给用户了。
如上图所示,用户提出让模型绘制一个正四棱台,模型理解了用户的意图并绘制出了一个美丽的图像,通过后端预处理逻辑被成功透传到公网目录中。
3. 总结
不管温度参数设定如何,由于 Python 语法的等效性,模型依然可能会不使用 print()
;或是 matplotlib 图像不显示。这时候就需要开发者在后端设计好相应的预处理、执行和后处理机制,就能有效地弥补 Agent 开发过程中的局限性,为模型“擦好屁股”。
53AI,企业落地大模型首选服务商
产品:场景落地咨询+大模型应用平台+行业解决方案
承诺:免费场景POC验证,效果验证后签署服务协议。零风险落地应用大模型,已交付160+中大型企业
2025-05-19
抛弃llama.cpp!Ollama自研引擎:本地推理性能飙升
2025-05-18
向量检索能力SOTA,字节Seed1.5-Embedding模型训练细节公开
2025-05-18
如何搭建自己的 MCP 服务器
2025-05-18
我也曾一上来就想微调大模型,直到我发现自己错得离谱!
2025-05-17
OpenAI发布GPT-4.1系列模型,对行业最大吸引力是什么?
2025-05-16
如何在 ONLYOFFICE 中离线使用 Ollama AI 模型
2025-05-16
英伟达新模型居然是微调千问,阿里源神称号实至名归
2025-05-16
应用流程文档(流程图+时序图):与Cursor AI协作的最佳语言
2025-02-04
2025-02-04
2024-09-18
2024-07-11
2024-07-09
2024-07-11
2024-07-26
2025-02-05
2025-01-27
2025-02-01