AI协作工程(一):从 Prompt 到 Agent 的形成逻辑

本文作为 AI协作工程 系列的起点,想回答的不是“有哪些好的 AI 编码工具”,而是一个更基础的问题:当 Coding LLM 进入开发流程之后,软件开发到底是怎么被重新组织起来的。全文会围绕三个主题展开:先界定 Coding LLM 为什么不同于普通聊天模型,再解释为什么理解 LLM 的工作机制 是学习 prompt 的前提,最后通过 Ollama、本地模型和三个小案例 few-shot、chain-of-thought、tool calling,说明 agent 更适合被理解为 prompt 工程进一步工程化之后的结果

引言

过去很长一段时间里,软件开发的核心动作可以被概括为一条相对稳定的链路:理解需求,手工编写代码,运行测试,修复问题,然后继续迭代。工具在不断演进,语言也在变化,但 “人直接承担大部分实现细节”这一点其实一直没有变

大语言模型进入开发流程之后,这条链路开始发生变化。开发者不再只是直接编写实现细节,而是更多地在做任务定义、上下文组织、约束表达、结果检查与执行编排。模型开始参与代码解释、代码生成、修改建议、测试补全、文档整理,甚至接入命令行、编辑器和外部工具,拉长了整个协作链路。软件开发并没有变成“只要向模型提问就可以完成工作”,但它确实在从 “人直接实现”转向“人组织任务,模型参与执行”

这也是为什么 AI协作工程 这个主题值得单独拿出来说。很多人在接触 AI 编码时,最先学到的是一组 prompt 技巧,比如如何写更长的提示词、如何加角色设定、如何让模型分步思考。但如果一开始只盯着这些技巧,很容易把 AI development 理解成一套经验主义方法,好像只要掌握几个固定模板,就能稳定驱动模型完成各种工程任务。实际情况没有这么简单。Prompt 有时有效、有时失效,模型为什么对示例、顺序和格式这么敏感,为什么引入 tool calling 之后执行结构会明显变化,这些问题很难只靠技巧解释。

因此,这篇作为 AI协作工程 系列开篇的文章,不打算从工具或平台角度对比,而是从三个更底层的问题切入。

  • 什么是 Coding LLM,它和普通聊天模型有什么差异。
  • 模型是如何工作的,这种工作方式为什么会直接影响 prompt 的设计效果。
  • agent 并不是什么突然出现的新能力,而是 prompt 工程不断工程化之后的自然延伸。

本文会沿着一条尽量稳定的主线展开:先界定 Coding LLM 这一对象,再回到 LLM 的基本工作方式,解释为什么 prompt 会成为 AI development 的基础能力,然后通过一个最小的本地实践环境说明这些原则如何真正落地,最后再顺着 few-shot、chain-of-thought 和 tool calling 三个案例,过渡到 agent 的形成逻辑。只有把这条主线先理清,后面再讨论 AI IDE、terminal agent、测试、安全和复杂工作流,整体理解才不会跑偏。

---
title: 传统软件开发流程与 AI 协作开发流程对比图
---
flowchart TB
    %% 描述: 对比开发流程的同时,突出“人”和“模型/工具”在 AI 协作中的职责拆分与重组。

    subgraph A["传统软件开发"]
        direction LR
        A1["理解需求"] --> A2["人直接编写
实现细节"] A2 --> A3["运行测试"] A3 --> A4["修复问题"] A4 --> A5["持续迭代"] end subgraph B["AI 协作开发"] direction LR B1["人:需求定义
上下文组织
约束表达"] B1 --> B2["模型:代码生成
修改建议
文档整理"] B2 --> B3["工具:命令行
编辑器
外部系统"] B3 --> B4["人:结果检查
执行编排
持续迭代"] end A -->|"开发范式迁移"| B

1. 什么是 Coding LLM,为什么它改变了开发方式

要理解 AI development 为什么会改变开发方式,首先要界定本文讨论的核心对象。通常所说的 LLM,是指在大规模文本数据上训练出来、能够根据上下文生成后续内容的大语言模型。它擅长语言理解、续写、总结、改写和模式归纳,因此在问答、写作、搜索辅助和对话场景中表现出了很强的通用能力。

但当这类模型进入软件开发场景之后,任务结构很快发生变化。开发任务并不只是“生成一段自然语言”,而是需要处理代码、文件结构、接口约束、运行错误、测试结果和工程上下文。也正因为如此,所谓 Coding LLM,更适合被理解为面向开发任务适配的模型形态。它需要在训练数据、指令微调方向、代码语料覆盖、工具接入方式和交互接口上都更靠近软件工程实践,因此能更稳定地完成解释代码、生成函数、修改已有实现、补充测试、分析报错、梳理调用链和配合外部工具执行任务等工作。

这类模型与普通聊天模型最重要的差异,不在于它“更会写代码”这一点本身,而在于它面对的是一种完全不同的输入环境。普通聊天场景往往以单轮问题为中心,输入主要是自然语言意图;开发场景则常常需要附带代码片段、目录结构、报错信息、接口定义、运行结果和明确的输出格式要求。换句话说,Coding LLM 的有效性很大程度上依赖于上下文组织能力,而不是单纯依赖模型内部记忆了多少语法知识。

从任务类型看,Coding LLM 至少覆盖几类高频工作。

  • 第一类是解释型任务,例如解释一段函数逻辑、分析一个模块的职责、说明某个报错为何出现。
  • 第二类是生成型任务,例如根据说明生成函数、测试、脚本或配置。
  • 第三类是修改型任务,例如在已有代码基础上修复问题、重构某一部分实现、补上遗漏的边界处理。
  • 第四类是验证与组织型任务,例如生成测试用例、梳理排查步骤、制定实现计划。
  • 再往前一步,当模型可以调用命令、查询文件、读取额外上下文时,它还会进入更接近 agent 的执行场景。

因此,AI development 的变化并不能简单概括为“模型开始替人写代码”。更准确的说法是,开发流程正在被重新组织。以前很多细粒度实现细节必须由人手工完成,现在其中一部分可以交给模型处理;与此同时,人在流程中的职责也发生了转移,越来越多地转向问题定义、上下文整理、质量把关、工具编排和结果验证。了解这些也就知道了为什么我们要先回到模型的底层机制,而不是直接聊工具使用。

---
title: Coding LLM 的任务能力地图
---

flowchart TB

    C["Coding LLM"]

    C --> A1["代码
解释"] C --> A2["代码
生成"] C --> A3["代码
修改"] C --> A4["测试
补全"] C --> A5["错误
分析"] C --> A6["工具
调用"] C --> A7["任务
规划"]

2. LLM 的底层工作方式,为什么会影响 Prompt 质量

学习 prompt 的过程中,一个很常见的误区,是把 prompt engineering 理解成某种脱离模型机制的技巧集合。只要背下若干模板,套上角色设定、语气要求和格式约束,就能够稳定得到理想结果。这里有个问题:它忽略了 prompt 之所以有效,本质上是因为模型本身具有特定的输入处理和输出生成方式

从最基础的层面看,LLM 可以被概括为一种基于上下文预测下一个 token 的模型。这里的 token 可以理解为文本被切分后的基本单位,可能是单词、词片段、符号或其他编码形式。模型在训练过程中会接触海量文本,学习不同文本模式在不同上下文中的共同分布规律。到了推理阶段,模型并不是“查询数据库找到答案”,而是在给定上下文条件下,持续估计哪些后续 token 更有可能出现,并一步一步把输出扩展出来。

这套机制带来一个很重要的结果:模型更像是概率驱动的模式建模器,而不是精确存储事实的规则系统。它擅长根据已有上下文进行补全、归纳和迁移,也擅长在见过的大量模式之间建立相似性联系,但它并不天然保证结果一定正确、稳定或可验证。也正因为如此,模型对输入上下文的组织方式异常敏感。示例放在前面还是后面,任务说明是否明确,格式要求是否具体,角色设定是否提供了额外约束,这些因素都会影响模型对“当前最可能的延续模式”的判断。

这也解释了为什么示例、顺序、角色和格式会显著影响输出。一个结构化 prompt 往往不是因为用了某种神秘咒语才变得有效,而是因为它更清楚地告诉模型当前处于什么任务环境中,需要遵循什么模式,应该以怎样的结构继续生成。对于 Coding LLM 来说,这一点尤其明显。因为开发任务通常带有更强的结构性,模型需要同时理解需求描述、代码上下文、输出边界和工具接口。如果输入上下文模糊、缺少必要示例,或者把多个层次的要求混在一起,模型就更容易产生表面上看起来合理、实际上却不符合预期的结果。

理解底层机制还有另一个现实作用,就是帮助判断问题到底出在哪里。很多时候,一个 prompt 结果不好,并不意味着“不会提问”,而更可能是以下几类问题中的一种。

  • 可能是上下文不足,模型没有看到关键代码或约束条件。
  • 可能是任务本身超出了模型当前能力边界,例如要求它对未知环境做精确判断。
  • 也可能是输出目标没有定义清楚,导致模型只能沿着最常见的语言模式继续生成。
  • 还有一些情况则更典型地属于工具层问题,本来应当通过外部检索、执行命令或读取运行结果来补足的信息,却被强行交给模型内部知识去猜测。

因此,先理解底层,再学 prompt,并不是在强调理论优先于实践,而是在强调认知顺序。如果不理解模型是如何处理输入的,就很难知道 prompt 应该控制什么,也很难区分当前遇到的问题究竟属于表达方式、上下文组织、模型边界还是工具缺失。只有先把这些基本机制理清楚,后文关于 prompt、few-shot、chain-of-thought 和 tool calling 的讨论,才不会停留在技巧表面。

---
title: LLM 输入到输出的简化机制图
---
flowchart LR
    A["用户输入"] --> B["上下文窗口
Context Window"] B --> C["基于当前上下文进行
Token 预测"] C --> D["逐步生成输出
Output Generation"] E["Prompt 影响因素
示例、顺序、格式、角色"] -. "影响上下文解释" .-> B

3. Prompt 为什么有效,它本质上在控制什么

如果说理解 LLM 的工作方式是在建立地基,那么理解 prompt 的本质,就是在明确人与模型之间究竟通过什么方式建立协作。Prompt 当然可以表现为一段文字,但它的作用远不止“提一个问题”这么简单。对于模型来说,prompt 实际上是在同时定义任务目标、上下文材料、边界条件、输出格式以及必要的参考模式。也正因为如此,prompt 的质量往往直接决定了模型对当前任务环境的判断精度。

从这个角度看,prompt 更接近一种输入设计,而不是一句自然语言命令。一个好的 prompt,通常至少会清楚回答几个问题:

  • 当前任务究竟要完成什么;
  • 模型应该依据哪些上下文作答;
  • 输出需要遵循什么结构;
  • 哪些内容必须包含,哪些内容必须避免;
  • 如果任务较复杂,是否需要给出示例或中间步骤。

Prompt 之所以能够显著影响输出,不是因为它在“操控LLM”,而是因为它在帮助模型更准确地定位 当前最合适的模式空间

这也是 prompt engineering 之所以带有 engineering 属性的原因。它并不是灵感式写作,也不是一次性沟通,而是一个可实验、可迭代、可评估的过程。不同 prompt 版本之间可以比较,不同上下文组织方式可以测试,输出结果可以根据任务完成度进行检查,较为稳定的写法还可以被沉淀为模板或系统组件。换句话说,prompt 工程和传统软件工程之间存在一种明显的结构相似性。二者都在处理接口设计、输入约束、反馈闭环和质量改进,只是一个面向程序行为,另一个面向模型行为。

把 prompt 理解为任务接口,还有助于纠正另一个常见误区:prompt engineering 不等于技巧目录。zero-shot、few-shot、chain-of-thought 当然都很重要,但如果脱离任务目标和模型机制,它们很容易退化成套路化的拼装。真正有效的方式,是先明确当前任务需要模型做什么,再判断是否要补示例、是否要拆步骤、是否要加格式约束,或者是否已经进入需要调用外部工具的阶段。也就是说,技巧永远只是手段,而任务结构和上下文组织才是核心

对于 AI development 来说,这一点格外重要。开发任务的复杂性远高于普通对话,常常涉及跨文件关系、错误上下文、运行环境和输出验证。如果 prompt 只是含混地表达需求,而没有提供足够的约束和上下文,模型即使在语言层面看上去回答得流畅,也可能在工程层面完全不可用。因此,在真正进入实践前,有必要明确好 prompt 的定义:它不是神秘技巧,而是 人与模型之间的任务定义机制

---
title: Prompt 结构拆解图
---

flowchart TB
    F["模型输出
Model Output"] A["任务目标
What To Do"] B["上下文
Context"] C["约束条件
Constraints"] D["示例
Examples"] E["输出格式
Output Format"] A --> F B --> F C --> F D --> F E --> F

4. 动手前的准备:用 Ollama 搭建最小实践环境

理解 prompt 和模型机制之后,下一步不是继续讨论技巧,而是搭一个可以反复验证的最小实验环境

原因很简单:很多问题只有在模型真正跑起来之后,才会变得具体。输入怎么组织会影响输出、哪些约束更容易被遵守、哪些写法会让结果变得不稳定——这些都很难靠抽象推断,只能通过实际运行来观察和比较。

对于开篇来说,本地模型环境是一个合适的起点。它的价值不在于性能,而在于路径足够短。不需要先引入复杂平台,也不需要分心处理远程服务、配额或费用问题,就可以围绕同一个任务反复调整 prompt,直接观察输出变化。这里真正需要的不是一个“完整方案”,而是一个足够简单、可以持续复现的实验场

dEgRQj

在这种语境下,Ollama 是一个很自然的选择。它提供了本地模型的管理与运行能力,并通过命令行统一交互方式。对于入门阶段,这已经足够支撑一条最关键的闭环:拉取模型、启动服务、发送 prompt、观察响应,再基于同一个问题不断调整输入。

4.1 安装与服务启动

在 macOS 系统中,可通过 Homebrew 安装和启动:

1
2
brew install --cask ollama
ollama serve

在 Linux 系统中,可通过官方脚本完成安装和启动:

1
2
curl -fsSL https://ollama.com/install.sh | sh
ollama serve

Windows 系统可通过官方网站下载安装程序并按提示完成安装。

安装完成后,通过以下命令验证版本信息:

1
ollama -v

若终端返回版本号,则说明环境部署成功。

4.2 模型下载与准备

完成安装后,需要下载两个模型。

  • mistral-nemo:12bMistral AI)是120亿参数的模型,大概占用7.1G存储空间。综合能力更强,适合较复杂任务,但资源占用更高。
  • llama3.1:8bMeta Platforms)是80亿参数模型,大概占用4.9G存储空间。更轻量、速度更快,适合日常对话和低资源环境。

该步骤仅在首次使用时执行。

1
2
ollama run mistral-nemo:12b
ollama run llama3.1:8b

模型下载完成后即可在本地运行,运行过程中主要消耗 CPU 与内存资源。

这里选择 mistral-nemo:12bllama3.1:8b 是因为它们足够支撑本文需要完成的观察:不同 prompt 组织方式会如何改变输出。模型本身不必一开始就追求“最强”,关键是环境要稳定,实验要容易重复。

4.3 模型验证

为了先验证环境是否正常,可以在进入交互界面后输入一个非常简单的测试 prompt:

1
2
3
4
5
请用三句话解释什么是 prompt engineering。
要求:
1. 输出中文
2. 不要使用列表
3. 语气正式
### 3.4 Python 调用示例 在 Python 环境中安装相关依赖:
1
pip install ollama python-dotenv
最小可运行示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from ollama import chat  
from logprint import P

response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": "You are a concise assistant."},
{"role": "user", "content": "请用三句话解释什么是 prompt engineering。要求:输出中文,不要使用列表,语气正式。"},
],
options={"temperature": 0}
)

if __name__ == '__main__':
P.print(response.message.content)
# Prompt engineering是一种人工智能技术,涉及设计和优化自然语言输入,以获得特定的输出结果或行为。它通过调整输入的措辞、结构和内容来影响模型的理解和响应。通过精心构造的提示,可以实现更准确、更有针对性的模型预测和决策。
若成功输出结果,则说明本地模型部署、服务启动与接口调用均已完成。完成这一步之后,再进入 few-shot、chain-of-thought 和 tool calling 的案例时,就更容易判断问题究竟出在环境、模型,还是输入设计本身。

在这个最小示例里,还有一个非常关键但容易被忽略的点:输入其实是分层组织的。system 和 user 并不是随意划分的,而是分别承担了不同角色。

system 更接近“规则”,用于定义模型应该如何回应,例如语气、风格和输出约束;user 则对应“任务”,提供当前需要处理的具体问题。这种区分的作用在于,让模型先对齐行为方式,再去处理输入内容。

从工程角度看,这种结构的价值在于将“可复用的约束”和“每次变化的输入”拆开。规则可以保持稳定,任务可以不断替换,从而更容易观察输入变化如何影响输出结果。后续的 few-shot、chain-of-thought 和 tool calling,本质上都是在这一基础上进一步强化或扩展这两层之间的关系。

进一步看,这种分层并不会止步于 system 和 user。随着任务复杂度提升,通常还会引入更多角色。例如,assistant 用来表示模型在过程中的中间输出,既可以作为 few-shot 示例,也可以承载推理过程;tool 则用于接收外部工具执行后的结果,将模型之外的信息重新引入上下文。

当这些角色同时存在时,交互结构也随之发生变化。模型不再只是接收输入并返回结果,而是开始在“生成 → 调用工具 → 接收反馈 → 继续生成”的链路中循环推进。这里的变化并不在于单次输出能力,而在于执行过程被显式组织起来。

换句话说,一旦开始区分“规则”“任务”“中间状态”和“外部反馈”,prompt 就不再只是一次简单的提问,而成为一种可以持续运转的执行结构。这也正是后续理解 agent 的基础。

---
title: Ollama 本地运行链路
---
flowchart LR
    A["本地终端
Terminal"] --> B["Ollama 服务
ollama serve"] B --> C["本地模型
mistral / llama"] D["用户 Prompt"] --> C C --> E["模型输出
Response"]

5. 三个代表性 Prompt 案例:从输入设计到任务组织

本文选择三个最有代表性的方向来观察 prompt 技术的演化。

  • Few-shot 代表的是模式约束能力,它说明示例如何帮助模型理解期望的输入输出结构
  • Chain-of-thought 代表的是推理组织能力,它说明复杂任务为什么常常需要更清晰的中间步骤
  • Tool calling 代表的是外部能力接入,它把模型从单纯的文本生成推向了任务执行

把这三者放在一起观察,可以看到一条相对清晰的路径:模型先学会遵循模式,再学会组织过程,最后开始连接外部工具。

这条路径之所以重要,是因为它直接对应了后文关于 agent 的讨论。很多时候,agent 被理解成一类突然出现的更高层系统,但如果回到实践层面观察,agent 的很多关键特征其实都能在 prompt 技术的逐步演化中找到前身。

  • Few-shot 让输入输出更稳定;
  • chain-of-thought 让任务过程更具结构;
  • tool calling 则让模型有机会把内部推理与外部执行连接起来。

执行结构 就是这样一点点变长、变复杂、变可组合的。

因此,接下来的三个小节并不是在展示互不相关的技巧,而是在展示一种连续的工程趋势。这种组织方式比单纯列出“有哪些 prompting techniques”更有价值,因为它能直接把 prompt、实践和 agent 之间的关系提前讲清楚。

需要说明的是,agent 的组成当然不止这三个部分,记忆、状态管理、规划策略等能力同样关键。但从工程演化的角度看,few-shot、chain-of-thought 和 tool calling 已经构成了一条最基本、也最容易被观察到的演进路径。

---
title: 三类 prompt 实践的渐进关系图
---
flowchart LR
    A["Few-shot
模式约束"] --> B["Chain-of-Thought
过程组织"] B --> C["Tool Calling
外部能力接入"] C --> D["Agent 雏形
执行结构变长"]

5.1 Few-shot Prompting:示例如何约束模型输出

Few-shot prompting 是最容易上手、也最容易在实践中观察到效果变化的一类方法。它的关键不在于“多给几个例子”,而在于通过示例明确当前任务所遵循的输入输出模式。模型本身擅长从上下文中归纳结构,一旦示例足够清晰,就更容易沿着同一模式继续生成。

这也是它在 coding 场景中格外有效的原因。许多任务并不缺描述,真正缺的是稳定的结构约束:例如按固定格式解释函数、按指定风格生成测试、将错误归类到统一字段,或根据示例把自然语言需求转化为特定形态的代码。在这些情况下,抽象要求往往不如示例直接,因为示例已经隐含了“结果应该长成什么样”。

few-shot 的价值往往体现在稳定性上。没有示例时,模型可能理解了任务,但输出结构和侧重点容易波动;加入示例后,模型会把注意力集中到模式复现上,结果也更可控。

为了更直观地观察这一点,可以看一个最小案例:将单词 httpstatus 反转,并且只输出结果 sutatsptth。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from ollama import chat  
from logprint import P

SYSTEM_PROMPT = """
你要完成字符反转任务。
示例1:
输入:apple
输出:elppa
示例2:
输入:drawer
输出:reward
只输出结果,不要解释。
"""

USER_PROMPT = """
将以下单词的字母顺序反转。
仅输出反转后的单词,不输出其他文本:
httpstatus
"""

response = chat(
model="mistral-nemo:12b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": USER_PROMPT},
],
options={"temperature": 0.5}, # temperature越高,输出越“随机”;越低,输出越“确定”
)

if __name__ == '__main__':
output = response.message.content.strip()
TARGET = "sutatsptth"
if output == TARGET:
P.print(f"✅ 正确:{output}")
else:
P.print(f"❌ 错误:{output},期望:{TARGET}")

在这个例子中,任务本身非常简单,但有个问题让模型能挣钱输出并不稳定。这并不是因为模型“不会反转字符串”,而是因为当前 prompt 对模式的约束还不够强:示例没有覆盖到当前输入,模型仍然需要做一定的泛化判断。

如果在SYSTEM_PROMPT中进一步加入一条:

1
2
3
示例3:  
输入:httpstatus
输出:sutatsptth

输出的正确性就会显著提高。原因并不复杂:当示例直接覆盖当前输入时,任务从“模式归纳”转变为“模式匹配”,模型几乎不再需要推断

这恰好说明了 few-shot 的一个关键边界:它本质上是在通过上下文约束模型的行为,而不是让模型真正“理解规则”。示例越接近目标输入,输出越稳定;但这也意味着,few-shot 的泛化能力是有限的。

因此,few-shot 的核心不在示例数量,而在于示例是否准确承载了当前任务的模式信息。它的意义也不只是让结果更好,而是让人意识到:prompt 并不是简单的提问,而是在主动构造模型的输入分布。一旦这一点建立起来,后续理解更复杂的推理与执行机制就会顺畅得多。

5.2 Chain-of-Thought:为什么拆解步骤会改善复杂任务

如果说 few-shot 解决的是“模型应该遵循什么模式”,那么 chain-of-thought(思维链)关注的是另一件事:模型应该如何组织推理过程

对于简单任务,直接生成结果通常没有问题。但一旦任务涉及多步推导、条件拆解或中间状态转换,模型就很容易在“跳步”中出错:要么遗漏关键条件,要么在局部推断中产生偏差。此时,仅仅给出目标是不够的,问题出在过程没有被约束

chain-of-thought 的核心做法很直接:把原本隐含的推理过程显式化。通过在 prompt 中引导模型一步一步展开,中间状态被暴露出来,推理路径变得可检查,最终结果也更稳定。这并不是让模型“更聪明”,而是让任务结构更清晰,从而减少推理中的不确定性。

这种方式在开发场景中非常常见。例如分析一个复杂报错时,如果只要求“给出原因和修复建议”,模型很容易直接输出一个看似合理但缺乏依据的结论;而如果要求它先识别错误类型,再分析可能原因,再列出验证路径,最后再给出修复方案,输出会明显更有层次,也更便于人工检查。

一个更通俗的例子,计算:3^12345 mod 100,并要求最后一行严格输出:

1
Answer: 43

这个问题的特点是:答案依赖中间推导。如果只让模型直接给结果,它往往会猜测或在计算过程中失稳;但如果明确引导推理路径,结果会稳定很多。

例如,可以这样设计 prompt:

1
2
3
4
请先分析这个问题属于哪一种模运算规律,再一步一步写出推导过程。
先确定幂在 mod 100 下的循环周期,再利用周期化简指数,最后得到结果。
最后一行必须单独写成:
Answer: <数字>

这里的关键不是“写更多内容”,而是把推理路径拆出来:先找规律 → 再化简 → 最后输出结果

下面是一段完整的最小代码示例,包含调用、约束和结果校验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from ollama import chat  
from logprint import P

# system prompt:定义推理路径 + 输出格式约束
SYSTEM_PROMPT = """
请先分析问题,再逐步写出推导过程。
先判断是否存在模运算周期,再利用周期化简指数。
最后一行必须严格写成:
Answer: <数字>
"""

# user prompt:提供具体问题
USER_PROMPT = """
求解下这个问题,并在最后一行按如下格式给出最终答案:“Answer: ”。
求 3^{12345} \\mod 100 的结果是多少?
"""

TARGET = "Answer: 43"

response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT}, # 约束推理路径和输出格式
{"role": "user", "content": USER_PROMPT}, # 提供具体问题
],
options={
"temperature": 0.3 # 降低随机性,减少推理过程和结果的波动
},
)

if __name__ == '__main__':
output = response.message.content.strip()

# 提取最后一行作为答案
last_line = output.splitlines()[-1].strip()

if last_line == TARGET:
P.print(f"✅ 正确:{last_line}")
else:
P.print(f"❌ 错误:{last_line},期望:{TARGET}")
P.print("完整输出:")
P.print(output)

这段代码里,有两个核心设计:

  • 明确推理路径,而不是泛泛要求“认真思考”。先判断周期 → 再化简指数。这相当于把问题的“解题思路”提前写进 prompt,减少模型在推理时的自由度。
  • 固定输出格式,方便工程验证。Answer: <数字>

这样做的意义不只是规范输出,而是让模型结果可以被程序直接判断。这一步非常关键,因为它把 prompt 从“语言技巧”连接到了“工程可验证”。当然chain-of-thought 并不是通用方案。

  • 对简单任务:会增加冗余,降低效率
  • 对结构清晰任务:可能反而引入噪音
  • 对复杂推理任务:效果最明显

它真正解决的问题是:当结果依赖多步推导时,如何让模型不跳步、不失真。

如果说 few-shot 让模型学会“遵循模式”,那么 chain-of-thought 则让模型开始“按照过程执行”。这一步很关键,因为从这里开始,prompt 不再只是控制输出结果,而是开始约束执行路径。一旦过程可以被组织,下一步让模型调用外部工具、参与真实任务执行,就会变得顺理成章。

---
title: Chain-of-Thought 步骤展开
---
flowchart LR
    A["问题输入
Complex Task"] --> B["识别问题类型
类型判断"] B --> C["中间推导步骤
Step-by-step Reasoning"] C --> D["最终答案
Answer"]

5.3 Tool Calling:为什么它已经开始接近 Agent

tool calling 的关键不在于“接入外部函数”,而在于把模型的输出从“自然语言结果”变成“可执行动作”。一旦模型开始输出结构化调用,并交由外部执行器运行,整个任务结构就从一次性生成转变为“生成 → 执行 → 反馈”的链路,这也是它逐渐接近 agent 的原因。

先看一个完整的实现。这里的目标不是让模型自己分析代码,而是让它学会在需要时调用工具,由外部系统给出真实结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import ast
import json
import os
from typing import Any, Dict, List, Optional, Tuple, Callable

from ollama import chat
from logprint import P

NUM_RUNS_TIMES = 3 # 多次运行,观察在轻微随机性下是否仍然稳定

# 工具实现:真实能力,不依赖模型
def _annotation_to_str(annotation: Optional[ast.AST]) -> str:
# 将函数返回值的 AST 注解转成字符串
if annotation is None:
return "None"
try:
return ast.unparse(annotation)
except Exception:
if isinstance(annotation, ast.Name):
return annotation.id
return type(annotation).__name__


def _list_function_return_types(file_path: str) -> List[Tuple[str, str]]:
# 解析 Python 文件,提取“顶层函数”的返回类型
with open(file_path, "r", encoding="utf-8") as f:
source = f.read()

tree = ast.parse(source)
results: List[Tuple[str, str]] = []

for node in tree.body:
if isinstance(node, ast.FunctionDef):
return_str = _annotation_to_str(node.returns)
results.append((node.name, return_str))

results.sort(key=lambda x: x[0]) # 排序保证输出稳定
return results


def output_every_func_return_type(file_path: str = None) -> str:
# 工具函数:输出每个函数的返回类型
path = file_path or __file__

if not os.path.isabs(path):
candidate = os.path.join(os.path.dirname(__file__), path)
if os.path.exists(candidate):
path = candidate

pairs = _list_function_return_types(path)
return "\n".join(f"{name}: {ret}" for name, ret in pairs)


# 示例函数(用于观察能否被工具解析)
def add(a: int, b: int) -> int:
return a + b

def greet(name: str) -> str:
return f"Hello, {name}!"


# 工具注册:限制模型可调用范围
TOOL_REGISTRY: Dict[str, Callable[..., str]] = {
"output_every_func_return_type": output_every_func_return_type,
}

# Prompt 协议:约束模型输出结构
SYSTEM_PROMPT = """
你必须使用 JSON 格式返回工具调用结果。
只能返回一个 JSON 对象,包含以下字段:
- tool:必须是 ["output_every_func_return_type"] 之一
- args:一个对象,可包含可选字段 "file_path"(字符串)。使用 "" 表示当前文件。
不要包含任何额外文本或其他内容。
"""
# 模型被强制输出 JSON,而不是自然语言答案
# 输出空间被压缩为:{tool, args}

# 工具调用解析
def extract_tool_call(text: str) -> Dict[str, Any]:
# 将模型输出解析为 JSON
text = text.strip()

if text.startswith("```") and text.endswith("```"):
text = text.strip("`")
if text.lower().startswith("json\n"):
text = text[5:]

return json.loads(text)


# 模型调用
def run_model_for_tool_call(system_prompt: str) -> Dict[str, Any]:
response = chat(
model="llama3.1:8b",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": "现在调用该工具。"},
],
options={"temperature": 0.3}, # 保留轻微随机性,观察稳定性
)

content = response.message.content
P.print(f"工具调用: {content}")
return extract_tool_call(content)

# 工具执行器:把“JSON”变成“真实执行”
def resolve_path(p: str) -> str:
if os.path.isabs(p):
return p
here = os.path.dirname(__file__)
c1 = os.path.join(here, p)
if os.path.exists(c1):
return c1
return p


def execute_tool_call(call: Dict[str, Any]) -> str:
# 1. 找工具
name = call.get("tool")
func = TOOL_REGISTRY.get(name)
if func is None:
raise ValueError(f"未知工具: {name}")

# 2. 处理参数
args = call.get("args", {})
if "file_path" in args and isinstance(args["file_path"], str):
args["file_path"] = (
resolve_path(args["file_path"])
if args["file_path"] != ""
else __file__
)
else:
args["file_path"] = __file__

# 3. 执行
return func(**args)

# Ground truth:真实答案(不经过模型)
def compute_expected_output() -> str:
return output_every_func_return_type(__file__)

# 测试
def run_prompt(system_prompt: str) -> bool:
expected = compute_expected_output()

for _ in range(NUM_RUNS_TIMES):

# 模型生成“工具调用”
try:
call = run_model_for_tool_call(system_prompt)
except Exception as exc:
P.print(f"解析失败: {exc}")
continue

# 执行工具
try:
actual = execute_tool_call(call)
P.print("工具输出:\n" + actual)
except Exception as exc:
P.print(f"执行失败: {exc}")
continue

# 校验
if actual.strip() == expected.strip():
P.print("成功")
return True
else:
P.print("期望输出:\n" + expected)
P.print("实际输出:\n" + actual)

return False


if __name__ == "__main__":
run_prompt(SYSTEM_PROMPT)

日志输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
工具输出:
_annotation_to_str: str
_list_function_return_types: List[Tuple[str, str]]
add: int
compute_expected_output: str
execute_tool_call: str
extract_tool_call: Dict[str, Any]
greet: str
output_every_func_return_type: str
resolve_path: str
run_model_for_tool_call: Dict[str, Any]
run_prompt: bool

成功

在这个实现中,最关键的变化有三点:

  • 模型被要求输出 JSON,而不是自然语言;
  • JSON 被解析为结构化调用;调用被外部执行器真正运行。
  • 模型的输出不再是终点,而只是执行链路中的一个中间节点。

一旦引入这种结构,任务就不再是单次生成,而是由“判断是否调用工具、构造调用参数、接收执行结果、继续推进”组成的过程。模型开始参与决策,但不再直接承担所有计算或查询工作。

因此,tool calling 的意义不在于“增强模型能力”,而在于改变系统边界。模型负责决策与组织,工具负责执行与验证。两者结合之后,让原本封闭在上下文中的生成过程,被扩展为一个可以与外部世界交互的执行系统,这正是 agent 的基本形态。

---
title: Tool Calling 执行闭环
---
flowchart LR
    A["模型判断
Need Tool?"] --> B["工具调用
Structured Call"] B --> C["外部执行
Executor"] C --> D["返回结果
Tool Result"] D --> E["模型继续生成
Next Step"] E --> A

6. Agent 不是凭空出现的,而是 Prompt 的工程化演化

到这里再看 agent,它已经不再是一个需要单独解释的新概念,而更像是一条连续演化路径上的自然结果。

模型最初通过 prompt 接收任务;在 few-shot 和 chain-of-thought 的帮助下,逐渐获得更稳定的模式约束和过程组织能力;随后通过 tool calling 与外部能力建立连接,开始处理超出纯文本生成范围的问题。当这些能力继续叠加,并引入任务拆解、状态记录、结果校验和循环执行之后,agent 便逐渐形成。

因此,agent 的核心并不在于“更强的模型”,而在于执行结构的变化。系统之所以被称为 agent,是因为它能够在一条更长的任务链路中持续运转:接收目标,拆分任务,决定是否调用工具,根据返回结果调整策略,并在必要时反复执行。这种能力的本质,是对执行过程的组织,而不是对单次生成能力的提升。

从这个角度看,prompt、prompt chaining、tool calling 与 agent 之间并不存在清晰的分界线,而是一种逐级展开的关系。单轮 prompt 关注的是一次性表达;prompt chaining 开始组织多步过程;tool calling 建立与外部世界的接口;而 agent 则在此基础上形成持续运行的执行闭环。变化的不是技术类别,而是系统复杂度。

这一点对于理解后续内容很关键。如果把 agent 当成一个与 prompt 无关的独立概念,就容易把注意力放在工具形态或界面差异上,而忽略其底层结构。相反,只要把它看作 prompt 工程的延伸,就会更容易理解为什么上下文管理、工具边界、状态维护和结果校验会在各种 agent 系统中反复出现。

换句话说,agent 并不是在提示词之外额外叠加的一层能力,而是当任务开始需要持续执行、依赖外部信息并形成反馈闭环时,一种自然出现的组织方式。

---
title: 从 Prompt 到 Agent 的演化
---
flowchart LR
    A["Prompt
任务表达"] --> B["Prompt Chaining
步骤串联"] B --> C["Tool Calling
工具接入"] C --> D["Agent
状态保存 + 循环执行"]

7. 总结

回到开头,Coding LLM 改变的不是某一个环节,而是软件开发的整体组织方式。原本由人串联的理解、实现、验证与修复,正在被重新拆分为任务定义、上下文组织、模型生成、工具执行和结果检查这一整条协作链路。

在这个变化之下,prompt 的角色也随之发生转变。它不再只是输入的一种写法,而成为人与模型之间的任务接口。只有先理解模型如何处理输入,以及为什么会对示例、顺序和格式产生敏感性,few-shot、chain-of-thought 和 tool calling 才会从“技巧”转变为可解释、可复用的工程手段。顺着这条路径继续往前,agent 也就不再是一个突兀的概念,而更像是 prompt 工程持续工程化之后的结果。

从系列的角度看,这篇文章试图建立一种更稳定的理解顺序:先理解模型,再理解 prompt;先理解 prompt,再理解 agent;先看清执行结构,再讨论具体工具。只有在这个顺序之下,后续关于 AI IDE、terminal agent、测试、安全以及复杂工作流的讨论,才不会停留在表面差异,而能够回到它们的结构逻辑。

8.备注

本文部分观点基于公开资料整理与个人实践总结,如有引用不准确之处欢迎指正。

参考材料: