Vor kurzem haben Benutzer auf X und Reddit berichtet, dass sie sich beim Schreiben von Code mit Claude 3.5 Sonnet „sehr mächtig“ fühlten. Ein solcher Beitrag, der besonders viel Aufmerksamkeit erregte, war ein Tweet von YCombinator-CEO Garry Tan.
Garry hat den folgenden Beitrag eines Mitglieds der YCombinator-Reddit-Community geteilt, in dem es darum geht, wie die Verwendung von Claude 3.5 Sonnet die Produktivität des Unternehmens um das Zehnfache steigerte und gleichzeitig beliebte Funktionen implementierte.
Der Beitrag betonte auch, wie wichtig es sei, fundierte architektonische und infrastrukturelle Entscheidungen zu treffen, die für die tägliche Nutzung des LLM-Studiums von entscheidender Bedeutung seien.
Obwohl LLMs wie Claude 3.5 herausragende Vorteile bieten, haben sie immer noch Einschränkungen, was die Beibehaltung von Gedächtnis und Kontext betrifft. Um diese Einschränkungen zu umgehen, können verschiedene Entwicklungsstrategien eingesetzt werden. Diese Strategien basieren auf den Grundlagen der Softwareentwicklung, die alle Entwickler mit der Zeit verfeinern, die aber oft leicht übersehen werden, wenn LLMs in einfachem Englisch angeboten werden.
Die Anwendung derselben Grundprinzipien, die Entwickler täglich verwenden und die der Reddit-Autor als Architekturentscheidungen bezeichnet, auf LLM-Interaktionen kann zu hochmodularem, skalierbarem und gut dokumentiertem Code führen.
Im Folgenden finden Sie einige wichtige Codierungsprinzipien und Entwicklungspraktiken, die auf die LLM-gestützte Softwareentwicklung angewendet werden können, zusammen mit praktischen Beispielen in Python:
Alle Eingabeaufforderungen in den Beispielen wurden mit Claude Sonnet 3.5 verwendet.
Um die Beschreibung der einzelnen logischen Komponenten für das LLM zu vereinfachen und genaue Komponenten zu erhalten, zerlegen Sie die Codebasis in kleine, gut definierte Komponenten.
# database.py class Database: def __init__(self, sql_connection_string): .... def query(self, sql): .... # user_service.py class UserService: def __init__(self, database): self.db = database def get_user(self, user_id): return self.db.query(f"SELECT * FROM users WHERE id = {user_id}") # main.py db = Database("sql_connection_string") user_service = UserService(db) user = user_service.get_user(123)
Um komplexe Implementierungen zu verbergen, verwenden Sie Abstraktionsebenen und konzentrieren Sie sich auf Abstraktionen höherer Ebene, indem Sie die Details für jede Komponente verwenden, die dabei aufgezeichnet werden.
# top-level abstraction class BankingSystem: def __init__(self): self._account_manager = AccountManager() self._transaction_processor = TransactionProcessor() def create_account(self, acct_number: str, owner: str) -> None: self._account_manager.create_account(acct_number, owner) def process_transaction(self, acct_number: str, transaction_type: str, amount: float) -> None: account = self._account_manager.get_account(acct_number) self._transaction_processor.process(account, transaction_type, amount) # mid-level abstractions class AccountManager: def __init__(self): def create_account(self, acct_number: str, owner: str) -> None: def get_account(self, acct_number: str) -> 'Account': class TransactionProcessor: def process(self, account: 'Account', transaction_type: str, amount: float) -> None: # lower-level abstractions class Account(ABC): .... class Transaction(ABC): .... # concrete implementations class SavingsAccount(Account): .... class CheckingAccount(Account): .... class DepositTransaction(Transaction): .... class WithdrawalTransaction(Transaction): .... # lowest-level abstraction class TransactionLog: .... # usage focuses on the high-level abstraction cart = ShoppingCart() cart.add_item(Item("Book", 15)) cart.add_item(Item("Pen", 2)) total = cart.get_total()
Um die Aufgaben bei der Inanspruchnahme von LLM-Hilfe überschaubarer zu gestalten, definieren Sie klare Schnittstellen für jede Komponente und konzentrieren Sie sich darauf, sie alle separat zu implementieren.
class PaymentProcessor(ABC): @abstractmethod def process_payment(self, amount: float, card_no: str) -> bool: .... class StripeProcessor(PaymentProcessor): # stripe specific implementation def process_payment(self, amount: float, card_no: str) -> bool: .... class PayPalProcessor(PaymentProcessor): # paypal specific implementation def process_payment(self, amount: float, card_no: str) -> bool: ....
Um Halluzinationen vorzubeugen und den Umfang zu begrenzen, konzentrieren Sie sich auf ein kleines Teilstück auf einmal und stellen Sie sicher, dass jede Klasse/Funktion eine einzelne, klar definierte Verantwortung hat. Entwickeln Sie inkrementell, um mehr Kontrolle über den generierten Code zu haben.
class UserManager: # user creation logic def create_user(self, username, email): ... class EmailService: # send welcome email logic def send_welcome_email(self, email): .... class NotificationService: # send sms notification def send_sms(self, username, email): ... # Usage user_manager = UserManager() email_svc = EmailService() user = user_manager.create_user("hacker", "[email protected]") email_svc.send_welcome_email("[email protected]")
Um LLMs die Beschreibung der Codestruktur zu vereinfachen und ihre Vorschläge zu verstehen, verwenden Sie klare und konsistente Benennungsregeln.
# classes: PascalCase class UserAccount: pass # functions and variables: snake_case def calculate_total_price(item_price, quantity): total_cost = item_price * quantity return total_cost # constants: UPPERCASE_WITH_UNDERSCORES MAX_LOGIN_ATTEMPTS = 3 # private methods/variables: prefix with underscore class DatabaseConnection: def __init__(self): self._connection = None def _connect_to_database(self): pass
Um spezifische Implementierungen basierend auf den Anforderungen zu generieren, erstellen Sie Skelettcode für allgemeine Codestrukturen und verwenden Sie diesen als Startercode.
# Todo item - pydantic Model class TodoItem(BaseModel): id: Optional[int] = None title: str description: Optional[str] = None completed: bool = False todo_db = [<insert mock data>] @app.get("/todos", response_model=List[TodoItem]) async def get_todos(): # TODO: retrieve all todos @app.get("/todos/{todo_id}", response_model=TodoItem) async def get_todo(todo_id: int): # TODO: retrieve specific todo @app.post("/todos", response_model=TodoItem) async def create_todo(todo: TodoItem): # TODO: create todo @app.put("/todos/{todo_id}", response_model=TodoItem) async def update_todo(todo_id: int, todo: TodoItem): # TODO: update todo @app.delete("/todos/{todo_id}") async def delete_todo(todo_id: int): # TODO: delete todo
Um das LLM zu unterstützen, schreiben Sie vor der Implementierung eine kurze Dokumentation für jede Komponente. Generieren Sie außerdem Docstrings, die die Eingabeaufforderungen in Zukunft mithilfe des LLM selbst leiten können.
prompt:
Use a Documentation Driven Development approach to write clear, concise docstrings for the following python function calculate_discount(price, discount_percentage) takes in - price (float), original price of the item. - discount_percentage (float), discount percentage range(0-100). returns - price after discount (float) raises - ValueError, when discount_percentage is out of range example - calculate_discount(100, 10) -> 10.0
Antwort:
def calculate_discount(price: float, discount_percent: float) -> float: """ Calculate the discounted price of an item. Args: price (float): original price for the item. discount_percentage (float): discount percentage range(0-100). Returns: float: The price after applying the discount. Raises: ValueError: If discount_percentage is not between 0 and 100. Example: >>> calculate_discount(100, 20) 80.0 """ if not 0 <= discount_percent <= 100: raise ValueError("Discount percent must be between 0 and 100") discount_amount = price * (discount_percent / 100) return price - discount_amount
Um LLMs bei der Implementierung zu helfen und insbesondere Randfälle zu behandeln, schreiben Sie selbst Tests oder verwenden Sie LLMs dazu, bevor Sie die eigentliche Funktion implementieren. LLMs führen Code nicht intern aus, aber vorherige Tests, um sicherzustellen, dass der generierte Code die Funktionsanforderungen erfüllt, können beim Auffinden von Fehlern in von LLMs generiertem Code hilfreich sein.
Testfall:
def is_palindrome(s: str) -> bool: """check if string is palindrome.""" return s == s[::-1] class TestPalindrome(unittest.TestCase): def test_palindrome(self): self.assertTrue(is_palindrome("racecar")) self.assertTrue(is_palindrome("A man a plan a canal Panama")) self.assertFalse(is_palindrome("hello")) self.assertTrue(is_palindrome("")) # Edge case: empty string
prompt:
given the test case <insert test>, use Document Driven Development to write a function for TestPalindrome
Antwort:
def is_palindrome(s: str) -> bool: """ Check if a string is a palindrome. A palindrome is a word, phrase, number, or other sequence of characters that reads the same forward and backward, ignoring spaces, punctuation, and capitalization. Args: s (str): The input string to check. Returns: bool: True if the input is a palindrome, False otherwise. Examples: >>> is_palindrome("racecar") True >>> is_palindrome("A man a plan a canal Panama") True >>> is_palindrome("hello") False >>> is_palindrome("") True """ # Remove non-alphanumeric characters and convert to lowercase cleaned = ''.join(char.lower() for char in s if char.isalnum()) # Compare the cleaned string with its reverse return cleaned == cleaned[::-1]
Um mehrere Implementierungsdetails mit dem LLM zu besprechen, pflegen Sie eine Bibliothek häufig verwendeter Entwurfsmuster als Referenz.
# Singleton pattern class DatabaseConnection: _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) # initialize database connection return cls._instance # Factory pattern class AnimalFactory: @staticmethod def create_animal(animal_type): if animal_type == "dog": return Dog() elif animal_type == "cat": return Cat() else: raise ValueError("Unknown animal type") # Observer pattern class Subject: def __init__(self): self._observers = [] def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self): for observer in self._observers: observer.update() # Adapter pattern class LLMAdapter: def __init__(self, llm_service): self.llm_service = llm_service def generate_code(self, prompt): llm_response = self.llm_service.complete(prompt) return self.extract_code(llm_response) def extract_code(self, response): pass
Erstellen Sie zur Gewährleistung von Qualität und Konsistenz eine Checkliste zur Überprüfung des von LLM generierten Codes.
# Code Review Checklist ## Functionality - [ ] Code performs the intended task correctly - [ ] Edge cases are handled appropriately ## Code Quality - [ ] Code follows project's style guide - [ ] Variable and function names are descriptive and consistent - [ ] No unnecessary comments or dead code ## Performance - [ ] Code is optimized for efficiency - [ ] No potential performance bottlenecks ## Security - [ ] Input validation is implemented - [ ] Sensitive data is handled securely ## Testing - [ ] Unit tests are included and pass - [ ] Edge cases are covered in tests ## Documentation - [ ] Functions and classes are properly documented - [ ] Complex logic is explained in comments
LLMs arbeiten am besten mit einer definierten Struktur. Entwickeln Sie daher eine Strategie, um Codierungsaufgaben in kleinere Aufgaben aufzuteilen. Ein organisierter Ansatz hilft dabei, funktionierenden Code zu generieren, ohne dass der LLM den generierten Code mehrmals korrigieren muss.
prompt:
I need to implement a function to calculate the Fibonacci number sequence using a Document Driven Development approach. 1. Purpose: function that generates the Fibonacci sequence up to a given number of terms. 2. Interface: def fibonacci_seq(n: int) -> List[int]: """ generate Fibonacci sequence up to n terms. Args: n (int): number of terms in the sequence Returns: List[int]: fibonacci sequence """ 3. Key Functionalities: - handle input validation (n should always be a positive integer) - generate the sequence starting with 0 and 1 - each subsequent number is the sum of two preceding ones - return the sequence as a list 4. Implementation Details: - use a loop to generate the sequence - store the sequence in a list - optimize for memory by only keeping the last two numbers in memory if needed 5. Test Cases: - fibonacci_seq(0) should return [] - fibonacci_seq(1) should return [0] - fibonacci_seq(5) should return [0, 1, 1, 2, 3]
Obwohl alle oben genannten Beispiele unkompliziert erscheinen, macht es im großen Maßstab einen großen Unterschied, wenn grundlegende Praktiken wie modulare Architektur und effektives Prompt Engineering befolgt werden und ein solider strukturierter Ansatz bei der LLM-gestützten Entwicklung verfolgt wird. Durch die Implementierung dieser Praktiken können mehr Entwickler die Vorteile der Verwendung von LLMs maximieren, was zu einer verbesserten Produktivität und Codequalität führt.
LLMs sind leistungsstarke Tools, die am besten funktionieren, wenn sie sich an leicht zu übersehenden Prinzipien der Softwareentwicklung orientieren. Ihre Verinnerlichung kann den Unterschied zwischen elegant erstelltem Code und einem zufällig generierten Big Ball of Mud ausmachen.
Das Ziel dieses Artikels ist es, Entwickler zu ermutigen, diese Praktiken bei der Verwendung von LLMs stets im Hinterkopf zu behalten, um qualitativ hochwertigen Code zu erstellen und in Zukunft Zeit zu sparen. Da sich LLMs ständig verbessern, werden die Grundlagen noch wichtiger, um das Beste aus ihnen herauszuholen.
Um tiefer in die Prinzipien der Softwareentwicklung einzutauchen, sehen Sie sich dieses klassische Lehrbuch an: Clean Architecture: A Craftsman's Guide to Software Structure and Design von Robert C. Martin.
Wenn Ihnen dieser Artikel gefallen hat, freuen Sie sich auf den nächsten, in dem wir uns ausführlich mit dem Workflow für LLM-gestützte Entwicklung befassen. Teilen Sie uns in den Kommentaren bitte alle anderen Konzepte mit, die Sie für wichtig halten! Vielen Dank.