Prompt Engineering as Architecture: Don't Let AI Fall Into Over-Engineering Traps
๐ฏ The Ultimate Paradox of AI Programming: The Smarter You Are, The More Complex Your Code Becomes
In AI programming, there's a counterintuitive phenomenon: the clearer your requirement description, the more complex the code AI generates.
Why? Because AI doesn't understand the cost of dependency maintenance. When it considers all possible edge cases, it falls into the trap of over-engineering, generating "perfect but unmaintainable" code.
During Tidepool Notes development, I summarized a set of "Prompt Engineering as Architecture" methodology, with the core principle: define interfaces first, then talk about implementation.
๐ง AI's Tendency to Over-Engineer: Why AI Always Overthinks?
1. The Problem of Considering Too Many Edge Cases
AI's Thinking Pattern:
User Requirement: Create a user login feature
AI's Overthinking:
- What if user forgets password? โ Need password reset feature
- What if password is too simple? โ Need password strength validation
- What if someone tries brute force? โ Need login failure lockout
- What if network is unstable? โ Need retry mechanism
- What if server crashes? โ Need circuit breaker
- What if data leaks? โ Need data encryption
- What if...
Result: A simple login feature becomes a complex system with 10 sub-modules.
Reality is: Your mini-program might only have 10 users, you don't need these "what if" scenarios at all.
2. AI Doesn't Understand Maintenance Costs
AI's Design Logic:
- โ High functional completeness
- โ Comprehensive error handling
- โ Good extensibility
- โ Extremely high maintenance cost
- โ Exploding code complexity
- โ Huge learning cost
AI's Blind Spot:
AI thinks: Add one more validation function โ 10 more lines of code โ No big deal
Reality: Add one more validation function โ Need testing โ Need documentation โ Need maintenance โ
Team members need to learn โ Future modifications might be needed โ Technical debt accumulates
3. Real Over-Engineering Cases
Case 1: Simple Message Storage Feature
Your Requirement: "Store user-sent messages"
AI's Over-Design:
// Complex message system generated by AI
interface MessageSystem {
// Message body
message: {
id: string
content: string
type: 'text' | 'image' | 'voice' | 'video'
metadata: {
device: string
location: GeoLocation
sentiment: SentimentAnalysis
priority: 'low' | 'medium' | 'high' | 'urgent'
}
}
// Message status
status: {
delivered: boolean
read: boolean
archived: boolean
deleted: boolean
retry_count: number
last_error?: ErrorInfo
}
// Version control
version: {
current: number
history: MessageVersion[]
conflict_resolution: ConflictStrategy
}
// Security related
security: {
encrypted: boolean
checksum: string
access_level: AccessLevel
audit_trail: AuditEntry[]
}
}
// Corresponding complex implementation (500+ lines of code)
class ComplexMessageService {
constructor(
private messageRepository: MessageRepository,
private encryptionService: EncryptionService,
private auditService: AuditService,
private sentimentService: SentimentService,
private geoService: GeoService,
private versionService: VersionService
) {}
async createMessage(input: CreateMessageInput): Promise<MessageSystem> {
// 50 lines of validation logic
// 30 lines of encryption processing
// 40 lines of sentiment analysis
// 20 lines of geolocation processing
// 60 lines of version control
// 80 lines of audit logging
// ...
}
}
Simple Design Actually Needed:
// Simple and direct design
interface Message {
id: string
sessionId: string
content: string
createdAt: Date
}
class MessageService {
constructor(
private messageRepository: MessageRepository,
private logger: Logger
) {}
async createMessage(sessionId: string, content: string): Promise<Message> {
try {
const message = await this.messageRepository.create({
sessionId,
content,
createdAt: new Date()
})
this.logger.info(`Message created: ${message.id}`)
return message
} catch (error) {
this.logger.exception('Failed to create message', error)
throw error
}
}
}
Comparison Results:
- Code lines: 500 lines vs 30 lines
- Number of dependencies: 6 vs 2
- Maintenance cost: extremely high vs extremely low
- Function satisfaction: 100% vs 100%
๐๏ธ Interface-First Principle: Define Contract First, Then Implementation
Why Must Define Interfaces First?
Traditional Development Problems:
Developer A: I'll implement user features first
Developer B: I'll implement messaging features first
2 weeks later: Discover interfaces of two modules are incompatible, need refactoring
Advantages of Interface-First:
Product Manager: Let's define all interfaces first
- User service interface
- Message service interface
- AI service interface
Then implement separately, ensuring compatibility
Golden Rules of Interface Design
Rule 1: Interface as Contract
- Once defined, cannot be changed arbitrarily
- All implementations must strictly follow
- Interface changes require team review
Rule 2: Simplicity First
- As few interfaces as possible
- As few parameters as possible
- Return value structure as simple as possible
Rule 3: Single Responsibility
- Each interface does only one thing
- Avoid composite operation interfaces
- Maintain atomicity of interfaces
Interface Design Practice in Tidepool Notes
Step 1: Define Core Interfaces
// types/interfaces.ts - Core interface definitions
// User-related interfaces
interface UserService {
create(userData: CreateUserInput): Promise<User>
findById(id: string): Promise<User>
findByWxCode(wxCode: string): Promise<User>
}
// Session-related interfaces
interface SessionService {
create(userId: string): Promise<Session>
findById(id: string): Promise<Session>
updateStatus(id: string, status: SessionStatus): Promise<void>
}
// Message-related interfaces
interface MessageService {
create(sessionId: string, content: string): Promise<Message>
findBySessionId(sessionId: string): Promise<Message[]>
}
// AI service-related interfaces
interface LLMService {
generateSuggestion(sessionData: SessionData): Promise<string>
}
Step 2: Design Prompts Based on Interfaces
// Prompt template: Constrain AI implementation based on interfaces
const promptTemplate = `
Please implement the corresponding service class based on the following interface definitions:
Interface Definitions:
${interfaceDefinition}
Constraint Conditions:
1. Strictly follow interface signatures, cannot change parameters and return values
2. Use TypeScript strict mode, prohibit any types
3. Function implementation not exceeding 50 lines
4. Error handling uses logger.exception + throw
5. Do not add features not defined in interfaces
Technology Stack:
- Database: Prisma + PostgreSQL
- Logging: Winston
- Validation: Simple if judgments, no additional libraries
Please implement the above interfaces.
`
Step 3: Constrain AI Behavior with Interfaces
// AI-generated implementation must comply with interface constraints
export class UserServiceImpl implements UserService {
constructor(
private userRepository: UserRepository,
private logger: Logger
) {}
// Strictly follow interface definition
async create(userData: CreateUserInput): Promise<User> {
try {
const user = await this.userRepository.create(userData)
this.logger.info(`User created: ${user.id}`)
return user
} catch (error) {
this.logger.exception('Failed to create user', error)
throw error
}
}
// Cannot add methods not defined in interface
// โ async validateComplexRules(): void { ... }
// โ async sendWelcomeEmail(): void { ... }
// โ async createBackup(): void { ... }
}
๐ฏ Architectural Methodology of Prompt Engineering
1. Three-Layer Prompt Architecture
First Layer: Requirement Layer (What)
Clearly state what to do, not how to do it
Examples:
โ Don't say: Implement an enterprise-grade user authentication system
โ
Say: Implement user registration and login features
โ Don't say: Design a high-performance message queue
โ
Say: Implement message sending and receiving
Second Layer: Constraint Layer (How Not)
Clearly define boundaries and limitations, prevent AI from over-expressing
Examples:
Constraint Conditions:
- Single function not exceeding 50 lines
- Do not use third-party libraries (except database ORM)
- Throw errors directly, do not provide fallback solutions
- Interfaces cannot be changed, implementations must strictly follow
- Do not consider future extensions, focus on current needs
Third Layer: Implementation Layer (How)
Let AI freely express within constraint range
Examples:
Technical Requirements:
- Use TypeScript strict mode
- Use Prisma for database operations
- Use Winston for logging
- Follow Repository pattern
2. Prompt Templates to Prevent Over-Engineering
Template 1: Feature Constraint Template
I want to implement [feature name], please strictly follow the following requirements:
## Interface Definitions (Unchangeable)
[Specific TypeScript interfaces]
## Business Rules (Must Follow)
1. Only implement features defined in interfaces, do not add extra features
2. Do not consider possible future expansion needs
3. Do not handle extreme edge cases
4. Throw errors directly, do not provide fallback solutions
## Technical Constraints (Mandatory Execution)
1. Single function not exceeding 50 lines
2. Do not use any types
3. Error handling: logger.exception + throw
4. Do not add new dependency packages
5. Keep code simple and direct
## Implementation Requirements
Please implement the corresponding class based on the above interfaces and constraints.
Template 2: Interface-First Template
We need to implement [module name], please design interfaces first, then implement features:
## Step 1: Interface Design
Please design interfaces that meet the following requirements:
- Function description: [Specific feature description]
- Input parameters: [Parameter list]
- Output requirements: [Return value description]
- Edge cases: [Normal situations to consider]
## Step 2: Implementation Constraints
After interface design is complete, I will confirm interface definitions, then ask you to:
1. Strictly implement based on interfaces
2. Do not add features not defined in interfaces
3. Keep code simple
4. Focus on core business logic
Please design interfaces first, do not implement code directly.
Template 3: KISS Constraint Template
Implement [feature name], must follow KISS principle:
## Prohibited Actions
โ Do not consider future extensions
โ Do not handle extreme edge cases
โ Do not use complex design patterns
โ Do not add extra configurations and options
โ Do not provide fallback solutions
โ Do not use third-party validation libraries
## Required Actions
โ
Directly implement core features
โ
Use simple if judgments for validation
โ
Throw errors directly
โ
Keep code readable
โ
Focus on current needs
## Reference Standards
If code exceeds 200 lines, it's over-engineered
If functions exceed 10, it's over-engineered
If configuration files are needed, it's over-engineered
Please implement the simplest version.
3. Prompt Strategy for Dependency Maintenance Costs
Strategy 1: Dependency Number Limitation
During implementation, please follow dependency minimization principle:
## Allowed Dependencies
- Database ORM: Prisma
- Logging: Winston
- Testing Framework: Jest
- Node.js built-in modules
## Prohibited Dependencies
- Any utility libraries (lodash, etc.)
- Any validation libraries (yup, zod, joi, etc.)
- Any HTTP client libraries (axios, etc.)
- Any state management libraries
## Dependency Selection Principle
If it can be implemented with native APIs, never use third-party libraries
If it can be solved with simple solutions, never use complex solutions
If additional dependencies are needed, must provide sufficient reasons.
Strategy 2: Complexity Control
Specific requirements to control code complexity:
## Function Complexity
- Single function not exceeding 50 lines
- Nesting level not exceeding 3 layers
- Single function parameters not exceeding 5
## Class Complexity
- Single class not exceeding 300 lines
- Number of methods not exceeding 10
- Dependency injection not exceeding 5
## File Complexity
- Single file not exceeding 500 lines
- Imported modules not exceeding 10
- Exported interfaces not exceeding 5
If exceeding above limits, please redesign simplified solution.
๐ช Practical Cases: Taming AI with Interface-First
Case 1: Interface-First Implementation of User System
Wrong Approach: Let AI Implement Directly
You: Help me implement a user system
AI (Over-Engineered):
- User registration, login, password reset
- Permission management, role assignment
- Operation auditing, login logs
- Email verification, phone verification
- Account locking, security policies
- User profiling, behavior analysis
- ...
Result: 800 lines of code, 10 dependencies, overly complex
Correct Approach: Interface-First
You: Let's define interfaces for the user system first
You: I need the following features:
1. User registration (username, password)
2. User login (username, password)
3. Get user info (user ID)
Please design TypeScript interfaces first, don't rush to implement code
AI (Interface Design):
interface UserService {
register(username: string, password: string): Promise<User>
login(username: string, password: string): Promise<User>
getUserById(userId: string): Promise<User>
}
You: Interface is good, now implement features based on this interface, constraint conditions:
- Single function not exceeding 30 lines
- Do not use any validation libraries
- Throw errors directly
- Do not add extra features
AI (Simple Implementation):
export class UserServiceImpl implements UserService {
constructor(
private userRepository: UserRepository,
private logger: Logger
) {}
async register(username: string, password: string): Promise<User> {
if (!username || !password) {
throw new Error('Username and password required')
}
try {
const user = await this.userRepository.create({ username, password })
this.logger.info(`User registered: ${user.id}`)
return user
} catch (error) {
this.logger.exception('Registration failed', error)
throw error
}
}
async login(username: string, password: string): Promise<User> {
const user = await this.userRepository.findByUsername(username)
if (!user || user.password !== password) {
throw new Error('Invalid credentials')
}
return user
}
async getUserById(userId: string): Promise<User> {
const user = await this.userRepository.findById(userId)
if (!user) {
throw new Error('User not found')
}
return user
}
}
Result: 80 lines of code, 2 dependencies, simple and clear
Case 2: Constrained Implementation of AI Integration Features
Problem: AI tends to over-engineer LLM integration
AI's Over-Engineering Tendency:
// Complex LLM integration that AI wants
class ComplexLLMService {
constructor() {
this.promptManager = new PromptManager()
this.conversationManager = new ConversationManager()
this.responseParser = new ResponseParser()
this.cacheManager = new CacheManager()
this.rateLimiter = new RateLimiter()
this.errorHandler = new ErrorHandler()
this.metricsCollector = new MetricsCollector()
}
async generateResponse(input: UserInput): Promise<AIResponse> {
// 100 lines of complex logic
// Including retry, caching, rate limiting, monitoring, etc.
}
}
Interface-First Constraint Implementation:
You: We need to integrate AI conversation features, please design interfaces first
AI: interface LLMService {
generateSuggestion(sessionMessages: Message[]): Promise<string>
}
You: Good, now implement this interface, constraint conditions:
- Only call OpenAI API, do not handle other service providers
- No caching, direct API calls
- No retry, throw error directly on failure
- Function not exceeding 30 lines
AI (Constrained Implementation):
export class LLMServiceImpl implements LLMService {
constructor(
private openaiClient: OpenAIClient,
private logger: Logger
) {}
async generateSuggestion(sessionMessages: Message[]): Promise<string> {
try {
const messages = sessionMessages.map(msg => ({
role: msg.isUser ? 'user' : 'assistant',
content: msg.content
}))
const response = await this.openaiClient.chat.completions.create({
model: 'gpt-4',
messages: messages,
temperature: 0.7,
max_tokens: 1000
})
return response.choices[0].message.content
} catch (error) {
this.logger.exception('Failed to generate suggestion', error)
throw error
}
}
}
Case 3: Preventing AI from Adding Unnecessary Features
Common Scenarios: AI likes to "thoughtfully" add extra features
You: Implement message saving functionality
AI (Might add extra features):
- โ
Message saving
- โ Message search (you didn't ask for this)
- โ Message categorization (you didn't ask for this)
- โ Message statistics (you didn't ask for this)
- โ Message export (you didn't ask for this)
- โ Message encryption (you didn't ask for this)
Preventive Prompts:
Please implement message saving functionality, strictly follow the following requirements:
## Only Implement Features
- Save messages to database
- Query messages by session ID
## Absolutely Do Not Add Features
โ Message search
โ Message categorization
โ Message statistics
โ Message export
โ Message encryption
โ Any other features not explicitly requested
## Interface Definitions
interface MessageService {
save(sessionId: string, content: string): Promise<Message>
getBySessionId(sessionId: string): Promise<Message[]>
}
Please implement strictly according to the above interface, do not add any extra features.
๐ Summary: Core Principles of Prompt Engineering as Architecture
Three Core Understandings
1. AI Doesn't Understand Maintenance Costs
- AI pursues functional completeness, humans pursue maintainability
- AI thinks "one more feature is no big deal," humans know "one more feature means more maintenance burden"
- Must use prompts to set maintenance cost constraints for AI
2. Interface as Architecture, Constraint as Quality
- Define interfaces first, then talk about implementation
- Use interfaces to constrain AI's behavior boundaries
- Use constraint conditions to control code complexity
3. Simplicity is the Highest Form of Complexity
- If it can be solved with 10 lines, never use 100 lines
- If it can be done with 1 interface, never use 3 interfaces
- If it can be implemented directly, never take detours
Practical Checklist
Prompt Design Check:
- Are interfaces clearly defined?
- Are clear constraint conditions set?
- Is functional scope limited?
- Are unnecessary features prohibited?
- Is complexity controlled?
AI Output Check:
- Does it strictly follow interface definitions?
- Are there extra features not defined in interfaces?
- Are code lines reasonable?
- Is number of dependencies minimized?
- Does it over-consider edge cases?
Architecture Quality Check:
- Is interface design simple and clear?
- Is implementation direct and simple?
- Is maintenance cost controllable?
- Is new team member understanding cost low?
- Are extensions within reasonable scope?
Remember These Golden Sentences
About AI's Nature:
- "AI is smart, but has no responsibility to maintain code"
- "AI pursues perfection, but reality needs 'good enough'"
- "AI doesn't know the human cost behind every feature"
About Architectural Thinking:
- "Interface as contract, agreement as quality"
- "Constraints are not limitations, but quality guarantees"
- "Simplicity is not crudeness, but the highest form of complexity"
About Practice Principles:
- "First talk about what to do, then how to do it"
- "First define interfaces, then write implementations"
- "First constrain boundaries, then unleash creativity"
Ultimately Remember: Good prompt engineering is to let AI showcase its talents on the right track, not let it run wild creating complexity.
Next Article Preview: Cognitive Traps in AI Programming: Why You Think It's Simple, AI Finds It Complex
Search "ๅๅฅ่ฟไผ่AI" on WeChat to see how these prompt engineering techniques create value in actual projects