主流模型提供商和开源运行时已支持直接传递JSON Schema约束输出,在logit级别防止模型生成非法数据,无需再写正则清洗markdown包裹的JSON。
如果你去年花了大量时间微调 LLM,只是为了得到一个没有被 markdown 反引号包裹整个内容的 JSON payload,你可能已经注意到目标在不断移动。
我们早已度过了生成式 AI 只是一个粘贴到 React 应用中的聊天机器人 API 的时代。工具链已经转向智能体工作流、本地执行,以及真正遵循你 schema 的结构化输入。如果你在过去六个月里没有关注这个生态,你的认知模型很可能已经过时了。
以下是目前实际对一线开发者重要的东西,去除了炒作周期。
还记得当年为了从 gpt-3.5 的回复中用正则表达式刮出 markdown 块而筋疲力尽吗?因为模型忽略了你关于原始 JSON 的 system prompt。这太让人崩溃了。
最近工具链最大的静默胜利是原生结构化输出。主流提供商和开源运行时现在允许你直接将 JSON schema 传递给推理端点。模型的 token 选择在 logit 层面就被约束了,所以它根本无法输出无效数据。
以下是使用现代 OpenAI SDK 配合 Pydantic 的样子。如果模型试图在应该返回整数的地方返回字符串,API 会在到达你的网络层之前就报错。
import os
from openai import OpenAI
from pydantic import BaseModel, Field
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
class CodeReview(BaseModel):
summary: str = Field(description="One sentence summary of the code quality")
bug_count: int = Field(description="Number of bugs found")
refactor_suggestions: list[str] = Field(description="List of specific improvements")
completion = client.beta.chat.completions.parse(
model="gpt-4o-2024-08-06",
messages=[
{"role": "system", "content": "You are a senior code reviewer."},
{"role": "user", "content": "Review this: `const x = eval(userInput);`"}
],
response_format=CodeReview,
)
review = completion.choices.message.parsed
print(f"Bugs found: {review.bug_count}")
print(review.refactor_suggestions)
这里的坑是:如果你通过 Ollama 或 vLLM 使用较老开源模型,仍然需要传递 grammar 文件(如 GBNF)或依赖 Instructor 这类库级别的约束。不要以为 response_format={"type": "json_object"} 能保证你的 schema 字段存在。它只是保证返回有效 JSON。如果想要真正的 schema 合规,请始终使用 Pydantic 的解析功能。
本地运行模型过去意味着看着风扇以最大速度旋转,同时一个 7B 参数模型需要四十秒来解释一个堆栈跟踪。
这一切已经改变了。随着 GGUF 这类量化格式以及 Ollama 和 llama.cpp 这类推理引擎的普及,在 Apple Silicon Mac 或不错的消费级 GPU 上运行 Llama 3.1 8B 或 Mistral 7B 这类模型已经非常快了。对于许多 CRUD 相关任务——分类、文本提取、简单的实体识别——你不再需要将用户数据发送到第三方 API 了。
以下是一个使用标准 fetch API 调用本地 Ollama 实例(运行 Llama 3.1)的 Node.js 脚本:
async function summarizeLocally(text) {
const response = await fetch('http://localhost:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'llama3.1',
prompt: `Summarize this error log in one sentence: ${text}`,
stream: false
})
});
if (!response.ok) {
throw new Error(`Local inference failed: ${response.statusText}`);
}
const data = await response.json();
return data.response;
}
summarizeLocally("TypeError: Cannot read properties of undefined (reading 'map') at UserList.jsx:42")
.then(console.log)
.catch(console.error);
但要注意:较小本地模型的上下文窗口和指令遵循仍然不够稳定。如果你的 prompt 依赖于复杂的多步推理,8B 模型会产生较大前沿模型能够轻松处理的虚假中间步骤。根据任务的实际复杂度来匹配模型大小,而不是根据你希望把所有东西都留在本地的欲望。
六个月前,人们使用庞大而固执的框架来构建智能体,这些框架在十二层类之后抽象掉了一切。一半的时间,你花在调试框架状态机上的时间比让 AI 做任何有用事情的时间还多。
现在的趋势是原始、简单的智能体循环。智能体本质上就是一个 while 循环,调用 LLM、检查模型是否想要调用工具、执行该工具,然后将结果反馈回上下文。
你不需要重型抽象。你只需要函数调用和基本的控制流。
import json
import requests
def get_current_weather(location: str):
# Stub for an actual weather API call
return json.dumps({"location": location, "temperature": "72", "unit": "fahrenheit"})
available_tools = {
"get_current_weather": get_current_weather
}
# The actual agent loop is remarkably dumb and simple
def run_agent_loop(initial_prompt):
messages = [{"role": "user", "content": initial_prompt}]
for _ in range(5): # Hard limit to prevent infinite loops
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=messages,
tools=[{
"type": "function",
"function": {
"name": "get_current_weather",
"parameters": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"]
}
}
}]
)
response_message = response.choices[0].message
messages.append(response_message)
if not response_message.tool_calls:
return response_message.content
for tool_call in response_message.tool_calls:
function_name = tool_call.function.name
function_to_call = available_tools[function_name]
function_args = json.loads(tool_call.function.arguments)
tool_output = function_to_call(**function_args)
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"name": function_name,
"content": tool_output,
})
这里的经典坑是 token 膨胀。随着循环迭代,历史记录不断增长。如果你的工具返回一个庞大的 JSON payload 或一个 500 行的日志文件,你的上下文窗口会瞬间填满,你的 API 成本会飙升,模型开始失去控制。在将工具输出塞回消息数组之前,总是先截断或总结它们。
从你当前技术栈中选择一个涉及混乱文本解析、人工分类或重复数据提取的部分。启动一个本地 Ollama 实例或获取一个 API key,用结构化输出写一个 30 行的脚本来解决它,然后看看模型实际上会在哪里失败。