PowerShell 技能连载 - 本地 LLM 工具链

适用于 PowerShell 7.0 及以上版本,需要 Ollama 或 LM Studio

背景引入

随着大语言模型(LLM)的快速发展,越来越多的开发者希望在自己的工作站上运行本地模型,以满足数据隐私、离线场景和低延迟的需求。Ollama 和 LM Studio 是目前最流行的两个本地 LLM 运行工具,它们都提供了兼容 OpenAI 格式的 REST API,可以方便地从任何编程语言调用。

PowerShell 作为 Windows 和跨平台运维的核心脚本语言,天然适合充当本地 LLM 的”胶水层”。通过 PowerShell 调用本地模型 API,我们可以将 AI 能力无缝嵌入到日常运维脚本、日志分析、文档处理等任务中,而无需将敏感数据发送到云端。

本文将介绍如何用 PowerShell 构建完整的本地 LLM 工具链,包括模型管理与对话接口、RAG 本地知识库,以及 AI 辅助运维工具三个核心场景。所有代码均基于本地 API,不依赖任何云服务。

Ollama API 集成

Ollama 提供了简洁的 REST API,默认监听 http://localhost:11434。下面的模块封装了模型列表查询、对话生成和流式输出等常用操作,可以作为日常脚本的基础工具集。

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
function Get-OllamaModel {
<# 获取本地已安装的模型列表 #>
$response = Invoke-RestMethod -Uri 'http://localhost:11434/api/tags' -Method Get
$response.models | ForEach-Object {
[PSCustomObject]@{
Name = $_.name
SizeMB = [math]::Round($_.size / 1MB, 1)
ModifiedAt = $_.modified_at
Family = $_.details.family
Parameter = $_.details.parameter_size
}
} | Sort-Object Name | Format-Table -AutoSize
}

function Invoke-OllamaChat {
<#
.SYNOPSIS
向本地 Ollama 模型发送对话请求
.PARAMETER Model
模型名称,如 qwen2.5:7b、llama3.1:8b
.PARAMETER Messages
消息数组,每条包含 role 和 content
.PARAMETER Stream
是否启用流式输出(默认 $false)
.PARAMETER Temperature
生成温度,0.0 到 1.0 之间
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Model,

[Parameter(Mandatory)]
[hashtable[]]$Messages,

[switch]$Stream,

[ValidateRange(0.0, 1.0)]
[double]$Temperature = 0.7
)

$body = @{
model = $Model
messages = $Messages
stream = $Stream.IsPresent
options = @{
temperature = $Temperature
}
} | ConvertTo-Json -Depth 5

if ($Stream) {
# 流式输出:逐行读取 SSE 响应
$request = [System.Net.Http.HttpRequestMessage]::new(
[System.Net.Http.HttpMethod]::Post,
'http://localhost:11434/api/chat'
)
$request.Content = [System.Net.Http.StringContent]::new(
$body, [System.Text.Encoding]::UTF8, 'application/json'
)

$client = [System.Net.Http.HttpClient]::new()
$response = $client.Send($request, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead)
$reader = [System.IO.StreamReader]::new($response.Content.ReadAsStream())

$fullResponse = [System.Text.StringBuilder]::new()
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if ($line) {
$chunk = $line | ConvertFrom-Json
if ($chunk.message.content) {
Write-Host $chunk.message.content -NoNewline
[void]$fullResponse.Append($chunk.message.content)
}
if ($chunk.done) { break }
}
}
Write-Host ''
$reader.Close()
$client.Dispose()
return $fullResponse.ToString()
}
else {
$result = Invoke-RestMethod -Uri 'http://localhost:11434/api/chat' `
-Method Post -Body $body -ContentType 'application/json'
return $result.message.content
}
}

# 用法示例:查看已安装模型
Get-OllamaModel

# 用法示例:单轮对话
$messages = @(
@{ role = 'system'; content = '你是一个 PowerShell 专家,回答简洁精准。' }
@{ role = 'user'; content = '如何获取系统中占用磁盘最大的 10 个目录?' }
)

$answer = Invoke-OllamaChat -Model 'qwen2.5:7b' -Messages $messages -Temperature 0.3
Write-Host "`n--- 模型回复 ---`n$answer"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Name              SizeMB   ModifiedAt               Family   Parameter
---- ------ ---------- ------ ---------
llama3.1:8b 4661.2 2026-04-18T10:30:00Z llama 8B
qwen2.5:7b 4368.5 2026-04-20T14:22:00Z qwen2 7B
deepseek-r1:7b 4520.8 2026-04-15T09:11:00Z qwen2 7B
nomic-embed-text 274.1 2026-04-10T08:00:00Z nomic 136M

--- 模型回复 ---

可以使用以下 PowerShell 命令获取占用磁盘最大的 10 个目录:

Get-ChildItem -Directory -Recurse -ErrorAction SilentlyContinue |
ForEach-Object {
$size = (Get-ChildItem $_.FullName -Recurse -File -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum).Sum
[PSCustomObject]@{ Path = $_.FullName; SizeMB = [math]::Round($size/1MB, 2) }
} | Sort-Object SizeMB -Descending | Select-Object -First 10

RAG 本地知识库

检索增强生成(RAG)是将本地文档作为上下文注入 LLM 的关键技术。下面的实现包含文档切片、向量化存储和检索查询三个步骤,完全使用本地模型(nomic-embed-text 做嵌入,对话模型做生成),无需外部服务。

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
class LocalVectorStore {
<# 本地向量存储,用于 RAG 检索 #>
[System.Collections.Generic.List[hashtable]]$Documents

LocalVectorStore() {
$this.Documents = [System.Collections.Generic.List[hashtable]]::new()
}

[void] AddChunk([string]$Content, [string]$Source, [double[]]$Embedding) {
$this.Documents.Add(@{
content = $Content
source = $Source
embedding = $Embedding
})
}

[double] CosineSimilarity([double[]]$A, [double[]]$B) {
$dot = 0.0; $normA = 0.0; $normB = 0.0
for ($i = 0; $i -lt $A.Count; $i++) {
$dot += $A[$i] * $B[$i]
$normA += $A[$i] * $A[$i]
$normB += $B[$i] * $B[$i]
}
if ($normA -eq 0 -or $normB -eq 0) { return 0 }
return $dot / ([math]::Sqrt($normA) * [math]::Sqrt($normB))
}

[hashtable[]] Search([double[]]$QueryEmbedding, [int]$TopK = 3) {
return $this.Documents | ForEach-Object {
[PSCustomObject]@{
Content = $_.content
Source = $_.source
Similarity = $this.CosineSimilarity($QueryEmbedding, $_.embedding)
}
} | Sort-Object Similarity -Descending | Select-Object -First $TopK |
ForEach-Object { @{ content = $_.Content; source = $_.Source; score = $_.Similarity } }
}
}

function Get-LocalEmbedding {
param([string]$Text)
$body = @{ model = 'nomic-embed-text'; prompt = $Text } | ConvertTo-Json
$result = Invoke-RestMethod -Uri 'http://localhost:11434/api/embeddings' `
-Method Post -Body $body -ContentType 'application/json'
return $result.embedding
}

function Split-Document {
param(
[string]$Content,
[int]$ChunkSize = 500,
[int]$Overlap = 50
)
$chunks = [System.Collections.Generic.List[string]]::new()
$words = $Content -split '\s+'
$i = 0
while ($i -lt $words.Count) {
$end = [math]::Min($i + $ChunkSize, $words.Count)
$chunkText = ($words[$i..($end - 1)] -join ' ')
[void]$chunks.Add($chunkText)
$i += $ChunkSize - $Overlap
}
return $chunks
}

function New-RAGIndex {
<# 从指定目录的文档构建 RAG 索引 #>
param(
[string]$Path = '.\docs',
[int]$ChunkSize = 500
)

$store = [LocalVectorStore]::new()
$files = Get-ChildItem -Path $Path -Include '*.md','*.txt' -Recurse

foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw -Encoding UTF8
$chunks = Split-Document -Content $content -ChunkSize $ChunkSize
$chunkIndex = 0
foreach ($chunk in $chunks) {
$embedding = Get-LocalEmbedding -Text $chunk
$store.AddChunk($chunk, "$($file.Name)#chunk-$chunkIndex", $embedding)
$chunkIndex++
}
Write-Host "已索引: $($file.Name) ($($chunks.Count) 个切片)" -ForegroundColor Cyan
}
Write-Host "索引完成,共 $($store.Documents.Count) 个文档切片" -ForegroundColor Green
return $store
}

function Invoke-RAGQuery {
<# 基于 RAG 索引进行检索增强查询 #>
param(
[LocalVectorStore]$Store,
[string]$Question,
[string]$Model = 'qwen2.5:7b',
[int]$TopK = 3
)

$queryEmbedding = Get-LocalEmbedding -Text $Question
$results = $Store.Search($queryEmbedding, $TopK)

$context = ($results | ForEach-Object {
"【来源: $($_.source) | 相关度: $($_.score):N3】`n$($_.content)"
}) -join "`n`n---`n`n"

$messages = @(
@{
role = 'system'
content = '根据以下参考资料回答用户问题。如果资料中没有相关信息,请明确说明。'
}
@{
role = 'user'
content = "参考资料:`n$context`n`n问题:$Question"
}
)

Write-Host "`n检索到 $($results.Count) 条相关文档:" -ForegroundColor Yellow
$results | ForEach-Object {
Write-Host " - $($_.source) (相关度: $($_.score):N3)" -ForegroundColor DarkGray
}

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.2
}

# 用法示例:构建索引并查询
$store = New-RAGIndex -Path '.\knowledge-base'
$answer = Invoke-RAGQuery -Store $store -Question '公司的密码策略要求是什么?'
Write-Host "`n$answer"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
已索引: security-policy.md (12 个切片)
已索引: operations-guide.md (8 个切片)
已索引: network-diagram.md (5 个切片)
索引完成,共 25 个文档切片

检索到 3 条相关文档:
- security-policy.md#chunk-2 (相关度: 0.892)
- security-policy.md#chunk-7 (相关度: 0.856)
- operations-guide.md#chunk-1 (相关度: 0.743)

根据安全策略文档,公司密码策略要求如下:
1. 密码长度不少于 14 个字符
2. 必须包含大写字母、小写字母、数字和特殊字符中的至少三类
3. 密码有效期 90 天,不可重复最近 12 次使用过的密码
4. 账户锁定策略:连续 5 次输入错误后锁定 30 分钟
5. 管理员账户必须启用多因素认证(MFA)

AI 辅助运维工具

将本地 LLM 与 PowerShell 运维脚本结合,可以构建智能化的日志分析、命令推荐和错误诊断工具。下面这个模块展示了几个实用的运维场景。

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
function Invoke-AILogAnalysis {
<# 使用本地 LLM 分析系统日志 #>
param(
[string]$LogPath = 'C:\Windows\System32\winevt\Logs\System.evtx',
[int]$MaxEvents = 50,
[string]$Model = 'qwen2.5:7b'
)

Write-Host "正在读取最近的 $MaxEvents 条事件日志..." -ForegroundColor Cyan
$events = Get-WinEvent -Path $LogPath -MaxEvents $MaxEvents -ErrorAction SilentlyContinue

if (-not $events) {
$events = Get-WinEvent -LogName System -MaxEvents $MaxEvents
}

$errorEvents = $events | Where-Object { $_.Level -le 3 } |
Select-Object TimeCreated, Id, LevelDisplayName, Message |
Format-Table -AutoSize | Out-String

$prompt = @"
以下是最近的事件日志(仅含错误和警告):

$errorEvents

请分析以下内容:
1. 是否存在异常模式或重复性错误
2. 可能的根因分析
3. 推荐的处理步骤
"@

$messages = @(
@{ role = 'system'; content = '你是一位资深的 Windows 系统管理员,擅长日志分析和故障排查。' }
@{ role = 'user'; content = $prompt }
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.3
}

function Get-AICommandSuggestion {
<# 根据自然语言描述推荐 PowerShell 命令 #>
param(
[Parameter(Mandatory)]
[string]$Description,

[string]$Model = 'qwen2.5:7b'
)

$messages = @(
@{
role = 'system'
content = '你是一个 PowerShell 命令助手。根据用户的自然语言描述,给出 1-3 个 PowerShell 命令方案。每个方案包含命令、参数说明和使用场景。只返回 PowerShell 代码和简短说明。'
}
@{
role = 'user'
content = $Description
}
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.4
}

function Repair-AIScriptError {
<# 使用本地 LLM 诊断和修复脚本错误 #>
param(
[Parameter(Mandatory)]
[string]$ScriptContent,

[Parameter(Mandatory)]
[string]$ErrorMessage,

[string]$Model = 'qwen2.5:7b'
)

$messages = @(
@{
role = 'system'
content = '你是一个 PowerShell 调试专家。分析脚本代码和错误信息,给出问题原因和修复后的完整脚本。'
}
@{
role = 'user'
content = "脚本代码:`n$ScriptContent`n`n错误信息:`n$ErrorMessage"
}
)

return Invoke-OllamaChat -Model $Model -Messages $messages -Temperature 0.2
}

# 用法示例 1:日志分析
$analysis = Invoke-AILogAnalysis -MaxEvents 100
Write-Host $analysis

# 用法示例 2:命令推荐
$suggestion = Get-AICommandSuggestion -Description '查找过去 24 小时内被修改的大于 100MB 的文件'
Write-Host $suggestion

# 用法示例 3:脚本错误修复
$badScript = @'
Get-Process | Where-Object { $_.WorkingSet -gt 100MB } | Select Name, CPU
'@

$errorMsg = 'Cannot compare "System.Byte[]" to "System.Int32"'

$fix = Repair-AIScriptError -ScriptContent $badScript -ErrorMessage $errorMsg
Write-Host $fix

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
正在读取最近的 100 条事件日志...

## 日志分析报告

### 异常模式
1. **重复性磁盘警告**:过去 2 小时内出现 15 次 Disk 警告(Event ID 51),
指向磁盘 \Device\Harddisk1\DR1,表明可能存在磁盘硬件问题。
2. **服务异常终止**:W32Time 服务在 04:30 和 05:15 两次意外终止(Event ID 7034)。

### 根因分析
- 磁盘警告可能与坏道或 SATA 线缆松动有关
- 时间服务崩溃通常与网络连接中断相关

### 推荐处理步骤
1. 立即运行 `chkdsk /r` 检查磁盘健康状态
2. 使用 `smartctl -a /dev/sda` 查看 SMART 信息
3. 检查 W32Time 服务依赖的网络连接
4. 运行 `w32tm /resync` 重新同步时间
1
2
3
4
5
6
7
8
9
## 推荐方案

### 方案一:使用 Get-ChildItem
Get-ChildItem -Path C:\ -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 100MB -and $_.LastWriteTime -gt (Get-Date).AddHours(-24) } |
Select-Object FullName, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}}, LastWriteTime |
Sort-Object SizeMB -Descending

适用于全盘扫描,结果详细但速度较慢。

注意事项

  1. 模型选择与硬件匹配:7B 参数模型至少需要 8GB 显存或 16GB 内存,13B 模型建议 16GB 以上显存。使用 ollama list 查看已安装模型,根据硬件配置选择合适的模型大小,避免因内存不足导致推理速度过慢或崩溃。

  2. API 兼容性:Ollama 和 LM Studio 都兼容 OpenAI API 格式,但端口号不同(Ollama 默认 11434,LM Studio 默认 1234)。切换工具时需要修改 URI 前缀,建议将基础 URL 配置为变量或环境变量,便于灵活切换。

  3. RAG 的切片策略:文档切片大小直接影响检索质量。过大的切片包含过多无关信息,过小的切片丢失上下文。对于技术文档建议 300-500 词,对于日志文件建议按时间窗口切分,每片保留完整的上下文信息。

  4. 流式输出与超时处理:长时间推理可能超过 Invoke-RestMethod 的默认超时。流式输出可以避免长时间等待无响应,但需要手动处理 SSE 数据流。建议为非流式调用设置 -TimeoutSec 120 或更长的超时值。

  5. 敏感数据保护:虽然使用本地模型避免了数据外传,但仍需注意日志中可能记录 API 请求内容。在生产环境中建议禁用 PowerShell 的脚本块日志记录(ScriptBlockLogging),并定期清理 Ollama 的会话历史。

  6. 模型回复的不确定性:LLM 生成的代码和建议可能存在错误。对于运维操作(特别是破坏性命令),务必先在测试环境验证,不要直接将 AI 生成的命令粘贴到生产终端执行。建议加入 -WhatIf 参数进行预检。

PowerShell 技能连载 - 本地大模型运维助手

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

数据安全和隐私合规要求使得很多企业无法使用云端 AI 服务。本地部署的大语言模型(如通过 Ollama 运行的 Llama、Qwen、DeepSeek 等)提供了完全内网的 AI 能力——日志智能分析、故障根因推理、脚本自动生成、配置合规审查,所有数据不出内网。结合 PowerShell 的系统管理能力和本地 AI 的推理能力,可以构建强大的智能运维助手。

本文将讲解如何使用 PowerShell 构建基于本地大模型的运维自动化工具。

Ollama 环境准备

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
# 检查 Ollama 状态
function Test-OllamaStatus {
try {
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -TimeoutSec 5
Write-Host "Ollama 运行中" -ForegroundColor Green
Write-Host "可用模型:" -ForegroundColor Cyan
foreach ($model in $response.models) {
$sizeGB = [math]::Round($model.size / 1GB, 2)
Write-Host " $($model.name) ($sizeGB GB) - 修改于 $($model.modified_at.Substring(0, 10))"
}
return $true
} catch {
Write-Host "Ollama 未运行" -ForegroundColor Red
Write-Host "启动方式:ollama serve" -ForegroundColor Yellow
return $false
}
}

Test-OllamaStatus

# 拉取模型
function Install-OllamaModel {
param([Parameter(Mandatory)][string]$ModelName)

Write-Host "拉取模型:$ModelName(可能需要较长时间)..." -ForegroundColor Cyan
$body = @{ name = $ModelName } | ConvertTo-Json

Invoke-RestMethod -Uri "http://localhost:11434/api/pull" `
-Method Post -Body $body -ContentType "application/json"
Write-Host "模型已下载:$ModelName" -ForegroundColor Green
}

# Install-OllamaModel -ModelName "qwen2.5-coder:7b"

执行结果示例:

1
2
3
4
Ollama 运行中
可用模型:
llama3.2:latest (2.02 GB) - 修改于 2025-07-15
qwen2.5-coder:7b (4.67 GB) - 修改于 2025-07-20

本地 AI 调用封装

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
function Invoke-LocalAI {
<#
.SYNOPSIS
调用本地 Ollama 模型
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$SystemPrompt = "你是一个 PowerShell 运维专家。回答简洁,用中文。",

[string]$Model = "qwen2.5-coder:7b",

[double]$Temperature = 0.3,

[switch]$ShowProgress
)

$body = @{
model = $Model
system = $SystemPrompt
prompt = $Prompt
stream = $false
options = @{
temperature = $Temperature
num_predict = 2048
}
} | ConvertTo-Json -Depth 5

if ($ShowProgress) {
Write-Host "思考中..." -ForegroundColor DarkGray -NoNewline
}

try {
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/generate" `
-Method Post `
-Body $body `
-ContentType "application/json; charset=utf-8" `
-TimeoutSec 120

if ($ShowProgress) { Write-Host "`r完成! " -ForegroundColor Green }

return $response.response
} catch {
if ($ShowProgress) { Write-Host "`r失败! " -ForegroundColor Red }
throw "AI 调用失败:$($_.Exception.Message)"
}
}

# 基础使用
$answer = Invoke-LocalAI -Prompt "如何在 PowerShell 中获取所有已停止的自动启动服务?" -ShowProgress
Write-Host $answer

执行结果示例:

1
2
3
4
完成!
可以使用以下命令获取所有已停止的自动启动服务:

Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -ne 'Running' }

智能日志分析

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
function Invoke-AILogAnalysis {
<#
.SYNOPSIS
使用本地 AI 分析日志文件
#>
param(
[Parameter(Mandatory)]
[string]$LogPath,

[int]$TailLines = 50,

[string]$Model = "qwen2.5-coder:7b"
)

Write-Host "分析日志:$LogPath" -ForegroundColor Cyan

$content = Get-Content $LogPath -Tail $TailLines -ErrorAction Stop
$logText = $content -join "`n"

$prompt = @"
请分析以下服务器日志,找出:
1. 关键错误和异常
2. 可能的根因
3. 建议的修复步骤

日志内容(最近 $TailLines 行):
$logText
"@

$analysis = Invoke-LocalAI -Prompt $prompt -Model $Model `
-SystemPrompt "你是一个资深运维工程师,擅长日志分析和故障排查。用中文回答,结构清晰。" `
-Temperature 0.2

Write-Host "`n=== AI 日志分析报告 ===" -ForegroundColor Cyan
Write-Host $analysis
return $analysis
}

# 分析应用日志
Invoke-AILogAnalysis -LogPath "C:\Logs\MyApp\app.log" -TailLines 30

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
分析日志:C:\Logs\MyApp\app.log

=== AI 日志分析报告 ===
1. 关键错误:
- 14:30:15 数据库连接超时(Timeout expired)
- 14:35:20 消息队列写入失败
- 14:40:30 服务健康检查失败

2. 可能根因:
- 数据库连接池耗尽,导致后续操作级联失败
- 连接池 Max Pool Size=100,但并发请求峰值达到 200+

3. 修复建议:
- 增大连接池:Max Pool Size=200
- 添加连接超时重试逻辑
- 检查是否有未正确关闭的数据库连接

配置合规审查

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
function Invoke-AIConfigReview {
<#
.SYNOPSIS
使用 AI 审查配置文件的安全性
#>
param(
[Parameter(Mandatory)]
[string]$ConfigPath
)

$content = Get-Content $ConfigPath -Raw -ErrorAction Stop
$fileName = Split-Path $ConfigPath -Leaf

$prompt = @"
请审查以下配置文件($fileName),检查:
1. 安全风险(硬编码密码、不安全的协议、过弱的加密)
2. 性能问题(不合理的超时、缓存配置)
3. 最佳实践建议

配置文件内容:
$content
"@

$review = Invoke-LocalAI -Prompt $prompt `
-SystemPrompt "你是安全审计专家,专注于配置文件安全审查。用中文回答,按风险等级排序。" `
-Temperature 0.1

Write-Host "`n=== 配置审查报告:$fileName ===" -ForegroundColor Cyan
Write-Host $review
}

# 审查配置文件
Invoke-AIConfigReview -ConfigPath "C:\MyApp\appsettings.json"

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
=== 配置审查报告:appsettings.json ===
安全风险(高):
- 发现硬编码的数据库密码:ConnectionString 中包含 P@ssw0rd
建议:使用环境变量或 Azure Key Vault

安全风险(中):
- CORS 配置允许所有来源:AllowAnyOrigin = true
建议:限制为具体域名

最佳实践建议:
- 建议启用 HTTPS 严格传输安全(HSTS)
- 日志级别在生产环境应为 Warning,当前为 Debug

批量运维辅助

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
# AI 辅助生成运维脚本
function Get-AIScript {
param(
[Parameter(Mandatory)]
[string]$Description
)

$prompt = @"
请根据以下描述生成一个 PowerShell 脚本。要求:
1. 包含参数验证
2. 包含错误处理(try/catch)
3. 使用 Write-Host 输出带颜色的状态信息
4. 代码简洁实用,不要过度设计

需求描述:$Description
"@

$script = Invoke-LocalAI -Prompt $prompt `
-SystemPrompt "你是 PowerShell 脚本专家。只输出代码,不要解释。" `
-Temperature 0.2

return $script
}

# 生成脚本
$generated = Get-AIScript -Description "监控多个远程服务器的 CPU 和内存使用率,超过阈值时输出告警"
Write-Host $generated

# AI 辅助解释命令
function Get-AIExplanation {
param([Parameter(Mandatory)][string]$Command)

$explanation = Invoke-LocalAI -Prompt "简要解释以下 PowerShell 命令的作用:$Command" `
-Temperature 0.1

Write-Host $explanation
}

Get-AIExplanation -Command "Get-ChildItem | Where-Object { $_.Length -gt 1MB } | Sort-Object Length -Descending | Select-Object -First 10 Name, @{N='SizeMB';E={[math]::Round($_.Length/1MB,1)}}"

执行结果示例:

1
这个命令在当前目录中查找大于 1MB 的文件,按大小降序排列,显示最大的 10 个文件的名称和大小(MB)。

注意事项

  1. 资源需求:本地 LLM 需要大量内存/显存,7B 模型至少需要 8GB 内存,13B 需要 16GB+
  2. 推理速度:没有 GPU 时推理较慢(CPU 模式每秒几 token),生产环境建议使用 GPU
  3. 模型选择:运维场景推荐代码类模型(Qwen-Coder、DeepSeek-Coder),通用能力更强
  4. 提示词工程:AI 输出质量取决于提示词质量,提供明确的上下文和格式要求
  5. 输出验证:AI 生成的脚本必须经过人工审查和测试,不能盲目执行
  6. 数据隐私:虽然数据不出内网,但仍需注意不要将敏感信息(密码、密钥)传入 AI

PowerShell 技能连载 - 本地大模型 Ollama 集成

适用于 PowerShell 7.0 及以上版本,需要安装 Ollama

前一篇我们探讨了 PowerShell 调用云端 AI API 的方式。然而在很多场景下——企业内网环境、数据合规要求、或者仅仅是不想为每一次调试付费——本地运行大语言模型是更务实的选择。Ollama 把模型下载、推理服务、REST API 打包成一条命令就能跑起来的体验,而 PowerShell 作为胶水语言,可以快速将这些能力集成到日常工作流中。

环境检查与模型下载

一切从确认 Ollama 是否可用开始。下面的函数会检测本地服务状态,并列出已下载的模型及其磁盘占用。

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
function Test-OllamaAvailable {
try {
# 尝试访问 Ollama 本地 API,获取已安装模型列表
$response = Invoke-RestMethod -Uri "http://localhost:11434/api/tags" -Method Get -TimeoutSec 5
$models = $response.models | Select-Object name, size, modified_at

if ($models) {
Write-Host "Ollama 已运行,可用模型:" -ForegroundColor Green
$models | ForEach-Object {
$sizeGB = [math]::Round($_.size / 1GB, 2)
[PSCustomObject]@{
Model = $_.name
SizeGB = $sizeGB
Modified = $_.modified_at
}
} | Format-Table -AutoSize
}
else {
Write-Host "Ollama 已运行,但尚未下载模型" -ForegroundColor Yellow
}
return $true
}
catch {
Write-Host "Ollama 未运行,请先执行: ollama serve" -ForegroundColor Red
return $false
}
}

Test-OllamaAvailable

执行后如果 Ollama 已经在运行,你会看到类似输出:

1
2
3
4
5
6
7
Ollama 已运行,可用模型:

Model SizeGB Modified
----- ------ --------
qwen2.5:7b 4.36 2025-04-02T10:15:00Z
qwen2.5-coder:7b 4.42 2025-04-01T18:30:00Z
llama3.2:3b 1.89 2025-03-28T09:00:00Z

如果 Ollama 正在运行但还没有模型,可以用下面的函数拉取你需要的模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Install-OllamaModel {
param(
[Parameter(Mandatory)]
[string]$Model = "qwen2.5:7b"
)

Write-Host "正在下载模型: $Model ..."
$process = Start-Process -FilePath "ollama" -ArgumentList "pull", $Model -NoNewWindow -Wait -PassThru

if ($process.ExitCode -eq 0) {
Write-Host "模型下载完成: $Model" -ForegroundColor Green
}
else {
Write-Host "下载失败,退出码: $($process.ExitCode)" -ForegroundColor Red
}
}

# 下载一个适合代码任务的模型
Install-OllamaModel -Model "qwen2.5-coder:7b"

首次下载根据网速需要几分钟到十几分钟不等:

1
2
3
4
5
6
正在下载模型: qwen2.5-coder:7b ...
pulling manifest...
downloading 8c47... 100%
verifying sha256...
writing layer...
模型下载完成: qwen2.5-coder:7b

基础对话

模型就绪后,我们先封装一个通用的对话函数。它封装了 Ollama 的 Chat API,支持系统提示词、温度控制和流式输出。

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
function Invoke-OllamaChat {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string]$Model = "qwen2.5:7b",

[string]$System = "你是一个有帮助的助手。",

[double]$Temperature = 0.7,

[switch]$Stream
)

# 构造请求体,包含系统提示和用户消息
$body = @{
model = $Model
messages = @(
@{ role = "system"; content = $System }
@{ role = "user"; content = $Prompt }
)
options = @{
temperature = $Temperature
}
stream = $Stream.IsPresent
} | ConvertTo-Json -Depth 5

if ($Stream) {
# 流式输出:逐块接收并拼接完整响应
$response = Invoke-WebRequest `
-Uri "http://localhost:11434/api/chat" `
-Method Post `
-ContentType "application/json" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

$lines = $response.Content -split "`n" | Where-Object { $_.Trim() }
$fullText = ""

foreach ($line in $lines) {
$json = $line | ConvertFrom-Json
$fullText += $json.message.content
}

return $fullText
}
else {
# 非流式:一次性返回完整内容
$response = Invoke-RestMethod `
-Uri "http://localhost:11434/api/chat" `
-Method Post `
-ContentType "application/json" `
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))

return $response.message.content
}
}

用一句话试试看效果:

1
Invoke-OllamaChat -Prompt "用一句话解释什么是 PowerShell 管道" -Model "qwen2.5:7b"
1
PowerShell 管道是一种将一个命令的输出对象直接作为下一个命令的输入的机制,使多个命令能够链式协作完成复杂任务。

代码生成助手

基础对话函数在手,我们可以针对特定场景定制系统提示词。下面这个函数专门用于生成 PowerShell 代码,回答风格被约束为”代码优先、注释清晰、包含错误处理”。

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
function Get-LLMPowerShellHelp {
param(
[Parameter(Mandatory)]
[string]$Question,

[string]$Model = "qwen2.5-coder:7b"
)

# 精心设计的系统提示,约束输出格式和质量
$systemPrompt = @"
你是 PowerShell 专家。回答要求:
1. 提供可直接运行的代码
2. 包含注释说明关键步骤
3. 包含错误处理
4. 优先使用 PowerShell 7 语法
"@

$answer = Invoke-OllamaChat -Prompt $Question -Model $Model -System $systemPrompt

Write-Host "`n$answer`n" -ForegroundColor Cyan

# 方便直接粘贴使用
Set-Clipboard -Value $answer
Write-Host "已复制到剪贴板" -ForegroundColor DarkGray
}

Get-LLMPowerShellHelp -Question "如何递归查找所有超过 100MB 的文件并导出 CSV?"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
以下脚本递归扫描指定路径,找出大于 100MB 的文件并导出到 CSV:

```powershell
# 设置搜索路径和输出文件
$SearchPath = "C:\Users"
$OutputCsv = "$HOME\large-files.csv"

# 递归查找大文件,跳过无权限的目录
$largeFiles = Get-ChildItem -Path $SearchPath -File -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Length -gt 100MB } |
Select-Object FullName,
@{ N='SizeMB'; E={ [math]::Round($_.Length / 1MB, 2) } },
LastWriteTime |
Sort-Object SizeMB -Descending

# 导出到 CSV(UTF-8 编码,Excel 友好)
$largeFiles | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding utf8

Write-Host "找到 $($largeFiles.Count) 个文件,已保存到 $OutputCsv"
````

日志分析

本地模型的一个实际用途是分析服务器日志。将日志尾部发送给模型,让它帮你识别错误模式、异常时间分布和可能的根因。

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
function Find-LogAnomaly {
param(
[Parameter(Mandatory)]
[string]$LogPath,

[int]$MaxLines = 200,

[string]$Model = "qwen2.5:7b"
)

# 读取日志尾部,避免一次性加载过大文件
$logs = Get-Content $LogPath -Tail $MaxLines -ErrorAction SilentlyContinue
if (-not $logs) {
throw "无法读取日志: $LogPath"
}

$logContent = $logs -join "`n"

# 结构化的分析提示词,引导模型给出可操作的结论
$prompt = @"
分析以下日志内容,找出异常和潜在问题:

$logContent

请列出:
1. 错误类型和出现次数
2. 异常时间模式
3. 可能的根本原因
4. 建议的处理措施
"@

$analysis = Invoke-OllamaChat -Prompt $prompt -Model $Model -Temperature 0.3

[PSCustomObject]@{
LogFile = $LogPath
Lines = $logs.Count
Analysis = $analysis
Timestamp = Get-Date
}
}

Find-LogAnomaly -LogPath "/var/log/nginx/error.log" | Format-List
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
LogFile   : /var/log/nginx/error.log
Lines : 200
Analysis : 日志分析结果如下:
1. 错误类型及次数:
- 502 Bad Gateway:47 次(集中出现)
- 504 Gateway Timeout:12 次
- 连接被拒绝:8 次
2. 异常时间模式:
- 502 错误集中在 14:00-14:30,可能是后端服务重启
- 504 超时在凌晨 03:00 左右出现,对应定时任务高峰
3. 可能的根因:
- 上游服务在 14:15 前后进行了部署,导致连接中断
- 凌晨定时任务消耗大量资源,引发超时
4. 建议:
- 在部署期间配置健康检查和重试机制
- 将资源密集型定时任务错峰执行
Timestamp : 2025/4/3 10:30:00

批量文档摘要

处理大量文档时,逐个阅读效率很低。下面的函数可以对一批文件自动生成摘要,适合快速了解项目文档、会议纪要或技术报告的核心内容。

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
function Get-LLMDocumentSummary {
param(
[Parameter(Mandatory)]
[string[]]$FilePaths,

[string]$Model = "qwen2.5:7b",

[int]$MaxContentLength = 8000
)

$summaries = foreach ($path in $FilePaths) {
if (-not (Test-Path $path)) {
Write-Warning "文件不存在: $path"
continue
}

# 读取文件内容,超长时截断以适配模型上下文窗口
$content = Get-Content $path -Raw -ErrorAction SilentlyContinue
if ($content.Length -gt $MaxContentLength) {
$content = $content.Substring(0, $MaxContentLength) + "...(已截断)"
}

$prompt = "请用 3-5 句话总结以下文档的核心内容:`n`n$content"

$summary = Invoke-OllamaChat -Prompt $prompt -Model $Model -Temperature 0.3

[PSCustomObject]@{
File = Split-Path $path -Leaf
Summary = $summary
}

Write-Host "." -NoNewline
}

Write-Host ""
return $summaries
}

# 示例:总结当前目录下的 README 文件
$readmes = Get-ChildItem -Path . -Filter "README*" -Recurse | Select-Object -ExpandProperty FullName
Get-LLMDocumentSummary -FilePaths $readmes | Format-List
1
2
3
4
5
6
7
8
9
...
File : README.md
Summary : 这是一个基于 Hexo 的技术博客项目,使用 NexT 主题。项目包含自动化部署脚本,
支持通过 GitHub Actions 实现持续集成。主要文章涵盖 PowerShell 技巧、
DevOps 实践和 AI 工具集成等内容。

File : README.en.md
Summary : English version of the project README, describing the blog setup,
theme configuration, and deployment workflow.

模型性能对比

不同模型在推理速度和回答质量上差异明显。下面的函数对同一问题在不同模型间做基准测试,帮助你选出最适合当前任务的模型。

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
function Compare-OllamaModels {
param(
[Parameter(Mandatory)]
[string]$Prompt,

[string[]]$Models = @("qwen2.5:7b", "llama3.2:3b", "gemma2:9b"),

[int]$Iterations = 1
)

$results = @()

foreach ($model in $Models) {
for ($i = 0; $i -lt $Iterations; $i++) {
$sw = [System.Diagnostics.Stopwatch]::StartNew()

$response = Invoke-OllamaChat -Prompt $Prompt -Model $model -Temperature 0
$sw.Stop()

# 记录每次推理的耗时和输出长度
$results += [PSCustomObject]@{
Model = $model
Run = $i + 1
TimeMs = $sw.ElapsedMilliseconds
Length = $response.Length
Response = $response.Substring(0, [Math]::Min(200, $response.Length)) + "..."
}

Write-Host "$model$($i+1) 次: $($sw.ElapsedMilliseconds)ms" -ForegroundColor DarkGray
}
}

# 按耗时排序输出对比表格
$results | Select-Object Model, Run, TimeMs, Length |
Sort-Object TimeMs |
Format-Table -AutoSize
}

Compare-OllamaModels -Prompt "解释 RESTful API 的核心设计原则" -Iterations 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
llama3.2:3b  第 1 次: 6230ms
llama3.2:3b 第 2 次: 5980ms
qwen2.5:7b 第 1 次: 12840ms
qwen2.5:7b 第 2 次: 12510ms
gemma2:9b 第 1 次: 21350ms
gemma2:9b 第 2 次: 20890ms

Model Run TimeMs Length
----- --- ------ ------
llama3.2:3b 1 6230 386
llama3.2:3b 2 5980 402
qwen2.5:7b 1 12840 512
qwen2.5:7b 2 12510 498
gemma2:9b 1 21350 623
gemma2:9b 2 20890 610

本地模型的推理速度取决于硬件配置。日常使用推荐 7B 参数量的模型(约 4GB 显存),代码任务优先选 coder 版本。如果有多张 GPU,设置 OLLAMA_GPU_LAYERS 环境变量可以优化推理性能。与云端 API 相比,本地模型的优势在于数据不出本机、无网络依赖、无调用计费;代价则是模型能力上限和并发处理能力有限。根据实际场景灵活选择,才是正解。