近期我意识到几个范式的变化,决定将大部分的实践精力都放在 AI 自动编程方面,一方面几个 AI 编程工具都实现了AI 代理模式,对 AI 代理和大模型的能力了解有帮助;另一个方面可以实践编程语言和软件工程。

在使用 AI 自动编程工具时,一个重要的技能是如何构造提示词,如果把握提示词的细节和精度。

我在实现一个 MCP Client 时,有了一些粗浅的心得。项目的背景是我用 typescript 语言调用 MCP 的 SDK 来实现一个 MCP 客户端,MCP Server 我选择了 sqlite Server,提供了多种工具,包括:

  • read_query
  • write_query
  • create_table
  • list_tables
  • describe_table
  • append_insight

在实现一些任务时,比如要查询某个表的数据,需要依次调用list_tables describe_tableread_query,和聊天工具的一问一答式的的交互不一样,模型端收到任务后,需要一直迭代,直到任务完成。类似Claude 客户端:

image.png

为了实现这个功能,我经历了三个版本。在这里,我的测试任务是:“先查询有哪些表,看下表的结构,然后查询表的前三条数据”

程序的第一个版本,只调用了两次模型,第一次是工具调用;第二次是普通调用。按理来说,应该不满足需求,但是客户端有模有样的输出所有信息,我很疑惑,把控制台的输出和代码提交给 Cursor,让它诊断下问题,结果好几轮都没有定位到问题,最后我比照了下数据库里的真实数据,才知道原来是模型的幻觉输出。

程序的第二个版本,我直接输出我的需求,让 Cursor 帮我构建程序,它先是将字符串是否包含“任务完成”的字样来判断,任务是否结束,但模型的输出不会那么严格遵循“任务完成”这样的输出,我让 Cursor改为结构性的输出,经过几轮的修改,如下:

const openaiResponse = await this.openai.chat.completions.create({

	model: 'anthropic/claude-3-sonnet',
	
	messages: [
		{
		role: 'system',
		content: `你是一个数据库助手。请基于用户的要求和之前的结果,决定下一步需要执行什么操作。
		如果需要执行工具,请先用一句话说明你要做什么,然后再使用工具。
		如果不需要执行工具,每次响应都必须包含 JSON 格式的状态信息:
		{
		"status": "continue" | "complete",
		"reason": "说明原因..."
		"result": "返回结果..."
		}
		示例:
		1. 执行工具:先说明意图,再使用工具
		2. 不执行工具:返回 {"status": "complete", "reason": "所有查询已完成","result": "结果为..."}`
		},
		...messages
	],
	temperature: 0.7,
	tools: availableTools.map(tool => ({
		type: 'function',
		function: {
		name: tool.name,
		description: tool.description,
		parameters: tool.input_schema
		}
	}))
});

第二个版本的输出还是有点问题,因为大模型并不是每次都遵循输出 JSON 格式的状态信息,需要对无法解析出 json 的情况做异常处理,也需要防止无限制的迭代下去,有个兜底的代码。当模型没有按照规范输出 JSON 格式,就会导致模型多交互一次。

// 处理状态响应
else if (assistantMessage.content) {
	try {
	// 尝试解析状态 JSON
		const statusJson = JSON.parse(assistantMessage.content);
		if (statusJson.status === 'complete') {
		console.log('\n✅ 完成:', statusJson.result);
		console.log('您还有哪些需求?');
		lastResult = statusJson.result;
		isTaskComplete = true;
		statusChecked = true;
		// 如果完成了,直接返回结果
		return lastResult;
	} else if (statusJson.status === 'continue') {
		console.log('\n⏳ 继续:', statusJson.reason);
		statusChecked = true;
	}
	} catch (e) {
		// 如果不是 JSON,当作普通响应处理
		if (assistantMessage.content.trim()) {
		console.log('💭 AI 说:', assistantMessage.content);
		lastResult = assistantMessage.content;
	}
}

	if (assistantMessage.content.trim()) {
		messages.push({
			role: "assistant",
			content: assistantMessage.content
			});
	}
}
// 如果没有检查到状态信息,可能需要提醒模型
if (!statusChecked) {
	messages.push({
	role: "user",
	content: "请明确指出当前任务的状态(complete/continue)"
	});
}
// 防止无限循环
if (stepCount > 10) {
	console.log('\n⚠️ 步骤数超过限制,强制结束');
	lastResult = '由于步骤数超过限制,执行被强制结束';
	isTaskComplete = true;
	return lastResult;
}

第三个版本,在第二个版本迭代了多轮之后,我意识到可能走到死胡同了,突然灵机一动,大模型输出内容如果不包括工具调用,不就是意味着可以结束循环了吗?所以我让Cursor 将代码的逻辑改为:判断模型的输出是否包含工具调用,作为继续迭代的判断条件。如下:

const openaiResponse = await this.openai.chat.completions.create({
	model: 'deepseek/deepseek-chat',
	messages: [
		{
			role: 'system',
			content: `你是一个数据库助手。请基于用户的要求和之前的结果,决定下一步需要执行什么操作。
			如果需要执行工具,请先用一句话说明你要做什么,然后再使用工具。
			如果不需要执行工具,直接返回最终结果。`
		},
		...messages
	],
	temperature: 0.7,
	tools: availableTools.map(tool => ({
		type: 'function',
		function: {
		name: tool.name,
		description: tool.description,
		parameters: tool.input_schema
		}
	}))
});

<代码省略...>
// 如果是普通文本响应,直接返回结果
else if (assistantMessage.content) {
	console.log('\n✅ AI 助手:', assistantMessage.content);
	return assistantMessage.content;
}

由于我的编程水平不高,第三个版本,可能不是最好的实现;第二种实现,我发现如果有专门的提取 JSON 字符串的程序来辅助也应该有不错的效果。

这里主要想表达的观点是,在使用自动编程中,当前还是需要有逻辑思维,基本的编程概念、产品设计能力和架构能力,可以控制模型生成代码的方向和思路;如果在一个错误的方法中让大模型反复迭代,会一直增加代码的复杂程度,同时也无法做到预期的效果。

接下来,我可能在实践过程中要一点点积累前端编程的名词概念、思维框架还有后端的模式设计之类,不断积累构造提示词的能力。

留下评论